Skip to content

Driver config reporting — stage 2: full DRIVER_CONFIG report - #968

Open
nikagra wants to merge 35 commits into
scylladb:scylla-4.xfrom
nikagra:feature/driver-config-reporting-phase2
Open

Driver config reporting — stage 2: full DRIVER_CONFIG report#968
nikagra wants to merge 35 commits into
scylladb:scylla-4.xfrom
nikagra:feature/driver-config-reporting-phase2

Conversation

@nikagra

@nikagra nikagra commented Jul 22, 2026

Copy link
Copy Markdown

What ☑️

Stage 2 (the payload) of driver configuration reporting: replaces the stage-1 {"version":1}
placeholder with the full DRIVER_CONFIG report — the effective configuration of the driver's
default execution profile plus the context's policies, serialized to the normative cross-driver JSON
schema shape. Stage 1 (#967) is merged; this branch is rebased onto scylla-4.x, so there is no
stage-1 noise in the diff.

Gated behind advanced.driver-config-reporting.enabled, which ships enabled (per
@dkropachev's cross-driver review). Turning it off suppresses only the DRIVER_CONFIG blob —
SESSION_ID rides on every connection unconditionally, independently of this flag, so "off" is not
"zero change on the wire".

Read the commits in order; each is formatter-clean and green on its own.

Latest push — five commits, from a review round that cross-referenced this branch against all
three siblings
(the 3.x port #974, scylladb/gocql#987, scylladb/csharp-driver#263). No
correctness defect survived it. What it did find: one traceability item, two places the
documentation is wider or narrower than the code, one test that never reached the path it names,
and four cross-driver resolutions now folded into the sections below.

  • The two places an operator reads before turning reporting off never said SESSION_ID keeps
    flowing.
    reference.conf and the option's own javadoc both stopped at "when false,
    DRIVER_CONFIG is not sent" — the asymmetry this description leads with was written down
    everywhere except there.
  • Widening BasicLoadBalancingPolicy#getLocalDatacenter/#getLocalRack to public breaks
    recompilation
    , though not linkage, of the subclasses the load-balancing manual invites: Java
    forbids an override reducing visibility. Now in the upgrade guide.
  • The 32 KiB cap was only ever exercised through the buildJson() seam, so nothing showed a
    report can reach it at all. It is now also reached from a datacenter name half the limit long,
    which is one of the unbounded user-supplied values the cap exists for. Same gap raised against
    Bump ch.qos.logback:logback-classic from 1.2.3 to 1.2.13 #263, where it was likewise only tripped by a test subclass.
  • Two javadoc bounds. No built-in policy ever infers a rackOptionalLocalRackHelper
    reads configuration only, and only once a datacenter is known — so the mixed node-preference
    variants serve a discoverLocalRack override rather than anything reachable today. And the two
    new SPI accessors' answers are inherited by subclasses of the built-ins rather than defaulting
    to empty, which is the one place this code delegates trust to an SPI instead of pinning an exact
    class.
  • One pre-existing one-character fix, in its own commit so it can be dropped:
    ConstantReconnectionPolicy formats a Duration with %d when rejecting a negative base-delay,
    so the operator gets a format error instead of the option name.

The report 🧩

Built from the default execution profile + the context's policies, and rebuilt on every
control-connection init
, so it always reflects the current (possibly runtime-reloaded) config.
Three groups: connection (connect timeout, request capacity, pooling, socket options,
reconnection policy, TLS when on, and the datacenter preference that scopes pooling),
control-plane (internal-query and schema-agreement timeouts), and query (per-request
defaults, plus the retry, load-balancing and speculative-execution policies).

Against the shipped default configuration, 938 bytes (pretty):

{
    "version": 1,
    "connection": {
        "connect": {
            "timeout-ms": 5000
        },
        "requests": {
            "in-flight": {
                "max": 1024
            },
            "orphaned": {
                "max": 256
            }
        },
        "pool": {
            "shard-aware": {
                "enabled": true
            }
        },
        "socket": {
            "tcp-no-delay": true,
            "keep-alive": false,
            "reuse-address": false
        },
        "reconnection": {
            "policy": {
                "type": "exponential",
                "base-ms": 1000,
                "max-ms": 60000
            }
        },
        "node-preference": {
            "type": "dc-auto"
        }
    },
    "control-plane": {
        "queries": {
            "system": {
                "timeout": {
                    "client-side-ms": 5000
                }
            }
        },
        "schema": {
            "agreement": {
                "timeout-ms": 10000
            }
        }
    },
    "query": {
        "defaults": {
            "page": {
                "size": 5000
            },
            "consistency": "LOCAL_ONE",
            "serial-consistency": "SERIAL",
            "idempotence": false,
            "client-timestamps": true,
            "request": {
                "timeout-ms": 2000
            }
        },
        "retry": {
            "policy": {
                "type": "standard-error-aware"
            }
        },
        "load-balancing": {
            "policy": {
                "type": "token-aware",
                "load-distribution": "shuffle",
                "fallback-to-non-preferred-nodes": false,
                "adaptive-ordering": {
                    "signals": [
                        "response-rate",
                        "in-flight-requests",
                        "recovery-state"
                    ]
                }
            },
            "node-preference": {
                "type": "dc-auto"
            }
        }
    }
}

Verified on the wire by a tshark capture of the STARTUP frames against a single-node CCM
ScyllaDB (protocol v4, default config): SESSION_ID on every connection, DRIVER_CONFIG only on the
control connection, and gone when the flag is off. Verified end to end through
system.clients.client_options (ScyllaDB 2026.1) and system_views.clients (Cassandra 4.1),
untruncated, with the one backend-conditional key differing as it should.

Invariants 🔒

  • Fail-safe. Any failure while building the report is swallowed and logged at WARN; SESSION_ID
    is still emitted and only the config blob is dropped. RuntimeException only — deliberately not
    bare Error, so OutOfMemoryError/StackOverflowError still surface. The one Error this class
    can provoke is the InternalError that getClass().getSimpleName() raises for certain synthetic
    classes; that is caught at the call site, behind a package-private seam so the branch stays
    testable.
  • Jackson can still be excluded. manual/core/integration documents that the driver "can operate
    normally without" Jackson, and DefaultDriverContext already honours that for Insights by checking
    DefaultDependencyChecker.isPresent(JACKSON) before building the listener. Reporting was a third
    Jackson user with no such check, and it is default-on and on the connection-init path — so on that
    classpath linking DefaultDriverConfigReporter raised NoClassDefFoundError, an Error raised
    while resolving the class rather than from any method it declares. Neither the try/catch above
    nor ProtocolInitHandler could contain it: every control connection failed and the session could
    not be built. Now the implementation is chosen up front, falling back to a NoopDriverConfigReporter
    that names no Jackson type anywhere (one reference would make loading it fail for exactly the
    deployments it exists to serve). Logged unconditionally, unlike Insights: nobody opted in to
    reporting, so nobody would think to look for a message saying it is off. Verified both ways against
    core's real runtime classpath with its Jackson jars removed.
  • The reporter is resolved during session init, alongside the policies DefaultSession.init()
    already forces eagerly. It was the one component on the reporting path left lazy, which made a
    Netty event loop the first thread to load it and Jackson — jar reads, mid-STARTUP. This is also
    what makes the ordering buildJson()'s javadoc relies on true by construction.
  • Size cap — 32KiB, matching gocql fix: release pre-acquired stream IDs #964 and csharp-driver Bump ch.qos.logback:logback-classic from 1.2.3 to 1.3.12 #262. Not just parity: STARTUP option
    values go through ByteBufPrimitiveCodec.writeString, which writes a 16-bit length prefix via
    ByteBuf.writeShort with no bounds check, so a value over 65535 bytes silently truncates the
    prefix modulo 65536 while still appending the whole body — a corrupt frame and a failed handshake,
    and not something the try/catch can save, since nothing throws. Parts of the report are
    user-supplied and unbounded (DC/rack names, consistency levels, custom policy class names), so
    without this the "reporting must never prevent a connection" invariant simply wasn't true. Measured
    on the UTF-8 bytes, since that is what the prefix counts.
  • Omission principle. A key the driver has no equivalent for is left out entirely, never emitted
    as null. Same where an optional key's configured value is outside what the schema can express
    (a disabled request timeout, a disabled SO_LINGER, an unbounded page size), and same where the
    answer is genuinely unknown — the two cases the schema made optional for exactly that purpose.
  • No missing config option costs more than the field it describes. Twelve reads used the
    no-fallback getters, which throw on an absent option, so a config source omitting any one of them
    dropped all ~34 fields behind a single WARN. Every read now either sits behind isDefined or
    passes an explicit fallback, and the schema picks which: an optional field falls back to the same
    "disabled" sentinel that already omits it, a required one to the value reference.conf documents.

Design decisions worth questioning 📐

  • Policy groups use exact-class discrimination, not instanceof — so a user subclass of a
    built-in falls through to {type:"custom", name:<class>} instead of being misreported as the
    unmodified built-in.
  • Policy parameters are read off the running instance, not the profile. connection.reconnection.policy
    and query.speculative-execution.policy describe the policy that is actually reconnecting and
    speculating. The built-ins latch these numbers into final fields when the context builds them, and
    advanced.speculative-execution-policy is documented as not modifiable at runtime, so a
    reloaded profile can carry values no request executes with — and, unlike a constructor, admits
    values the schema rejects: a negative delay-ms, or a max-executions of 1 that would drop the
    whole group while the policy still speculates. Reading the instance makes those ranges hold by
    construction. adaptive-ordering and fallback-to-non-preferred-nodes were the last two reading
    the profile — the class javadoc used to name them as the exception — and now read
    DefaultLoadBalancingPolicy.isAvoidingSlowReplicas() and
    BasicLoadBalancingPolicy.getMaxNodesPerRemoteDc() instead, so no latched value is described from
    a profile the running policy has not adopted. Raised by @dkropachev for the first; the second is
    the same defect one field over, fixed alongside it.
  • control-plane.queries.system.timeout.server-side-ms reports configuration, not effect.
    CassandraSchemaQueries adds a USING TIMEOUT clause built from advanced.metadata.schema.request-timeout
    only where shouldApplyUsingTimeout() sees sharding info, so on generic Cassandra the option is a
    client-side wait alone. This was gated on that signal until @dkropachev asked for the configured
    value regardless of peer detection — the way pool.shard-aware.enabled already reports intent. The
    cost is that an operator on Cassandra 4.1 reads a server-side timeout nothing enforces, which wants
    the schema description to say so; the gain is that the report no longer depends on anything the
    peer said, which removed the NodeShardingInfo argument entirely.
  • Whether TLS is on reads getSslHandlerFactory(), not getSslEngineFactory() (per
    @sylwiaszunejko). The handler factory is the reference ChannelFactory installs the SSL handler
    from, and buildSslHandlerFactory() is the documented expert extension point (e.g. Netty's native
    OpenSSL): an override supplies no engine factory, so reading the engine factory reported such a
    session as plaintext when it is in fact encrypted. Host name validation is then read off the engine
    factory the active handler actually wraps — never through the context, which can name a different,
    unused one and whose LazyReference the reporter would be the first to force (keystore reads on a
    Netty event loop, mid-STARTUP).
  • Two new SPI accessors, both tri-state. SslEngineFactory.isHostnameValidationRequired() and
    TimestampGenerator.isClientSide() return Optional<Boolean>, empty by default. Host name
    validation is a property of the JDK SSLEngine, unreadable through an opaque handler factory; and
    a custom TimestampGenerator is free to return Statement.NO_DEFAULT_TIMESTAMP and delegate to
    the coordinator, which no class check can detect and which calling next() to find out would have
    side effects. Both keys are now optional in the schema with absence defined as unknown, so an
    implementation that cannot answer is reported by omission rather than by a guessed boolean — which
    for these two fields would misdescribe a security control and a write-timestamp source. Both
    methods are default, so existing implementations keep compiling. Not a Java-local flourish:
    @dkropachev asked Bump ch.qos.logback:logback-classic from 1.2.3 to 1.2.13 #263 for exactly this shape on both fields — emit the boolean only where it
    is known, omit it for custom or unknown behaviour — and cited this PR's timestamp accessor by
    name as the model. (He also asked Bump ch.qos.logback:logback-classic from 1.2.3 to 1.2.13 #263 to derive client-timestamps from the negotiated
    protocol, since SupportsTimestamp() starts at v3; Java 4.x supports nothing below v3, so there
    is nothing to gate on here.) One thing the javadocs now spell out: a subclass of a built-in
    inherits its parent's answer rather than the empty default, so a subclass that changes what
    these describe has to override them too.
  • Sub-millisecond durations floor at 1 ms. The schema counts whole milliseconds while the driver
    holds these options as Duration and schedules several in nanoseconds, so truncating a 500 µs
    timeout to 0 would report a live timeout as the very value the field defines as off. Applies to
    schema.agreement.timeout-ms, queries.system.timeout.client-side-ms,
    query.defaults.request.timeout-ms and reconnection.policy.delay-ms. Three fields are
    deliberately exempt, because 0 is what they really mean there: connection.connect.timeout-ms
    (Netty's CONNECT_TIMEOUT_MILLIS truncates identically, and 0 disables it), ...server-side-ms
    (the value goes on the wire as a USING TIMEOUT millisecond argument, so sub-millisecond really
    is 0ms server-side) and speculative-execution.policy.delay-ms (reference.conf documents
    sub-millisecond delays as equivalent to 0).
  • connection.requests.orphaned.max is the effective threshold, not the configured one.
    ChannelFactory requires max-orphan-requests to stay below max-requests-per-connection and
    silently substitutes a quarter of the latter otherwise. Reporting the configured value would
    describe a threshold no connection was built with, so the correction lives in one place —
    ChannelFactory.effectiveMaxOrphanRequests(), which the channel setup itself calls.
  • The two node-preference slots are filled differently, because in Java they mean different
    things. computeNodeDistance derives node distance from the local DC alone — a node outside it is
    IGNORED, and an IGNORED node gets no pool — so the datacenter genuinely scopes which nodes are
    connected to, and goes under connection.node-preference. The rack never reaches that method: it
    only reorders replicas at the head of a query plan, with connections still held across the whole
    local DC. So the full preference (rack included) goes under query.load-balancing.node-preference,
    and the connection group carries the datacenter half alone. Emitting the same object in both would
    claim a rack-scoped connection pool that does not exist.
  • load-distribution is shuffle and adaptive-ordering maps to slow-replica avoidance. The
    built-ins shuffle the replica head of every query plan unconditionally
    (BasicLoadBalancingPolicy.shuffleHead, no config to disable), so round-robin would describe only
    the non-replica tail and replica-set would claim the order is untouched (see A1 for the one case
    this misses). Java has no latency-percentile ordering, so adaptive-ordering maps to the one real
    mechanism, DefaultLoadBalancingPolicy's slow-replica avoidance, with its signals read off
    avoidSlowReplicas rather than guessed — and latency deliberately absent, since those samples
    record when responses arrived, not how long they took. Its presence is also now the only thing
    distinguishing BasicLoadBalancingPolicy in the report, which has no such mechanism at all.

Spec conformance 🔍

The v1 schema is shipped verbatim as a test resource, byte-identical to the design document's
schema block, and every representative report is validated against it in
DefaultDriverConfigReporterTest — enforced, not asserted. A negative test confirms the validator
actually rejects an out-of-schema document.

Every report a stock configuration can produce validates. Two required fields are constrained more
tightly than the option behind them, but only one is reachable through a running driver. Both are
reported truthfully and pinned by tests that assert the violation:

  • query.defaults.consistency is a closed enum while basic.request.consistency is an unvalidated
    string. The built-in load balancing policies resolve it through the ConsistencyLevelRegistry in
    their constructor, so an unknown name fails the session before any report exists — reaching this
    needs a custom registry defining extra names, which is the case CodeRabbit raised. This is the
    one real gap.
  • connection.requests.in-flight.max must be positive, and nothing validates
    advanced.connection.max-requests-per-connection against that — ChannelFactory hands the value
    straight to StreamIdGenerator, which does not range-check it. An earlier revision of this
    description claimed such a setting starts a session; it does not.
    The connection fails first: a
    negative value makes StreamIdGenerator's BitSet throw while ChannelFactory is still building
    the channel, and 0 leaves no stream id for the control connection's own OPTIONS, which
    ChannelHandlerRequest fails on preAcquireId before STARTUP is composed. So this is unreachable
    by construction, not a live exposure. The value is still passed through and still pinned, so the
    behaviour stays defined if the driver ever stops failing that early. (The same setting would also
    drive orphaned.max negative — a second reason to read it as one unreachable shape rather than one
    field's gap.) Worth one cross-driver note, since Bump ch.qos.logback:logback-classic from 1.2.3 to 1.2.13 #263 was asked to change this very field:
    there the reported number was the pool-admission threshold rather than the stream-id pool, and
    @dkropachev asked for Connection.GetMaxConcurrentRequests (128 or 2048) instead. In Java the
    two are one number — ChannelFactory is new StreamIdGenerator(maxRequestsPerConnection)
    so the configured value already is the stream-id pool size and needs no such correction.

A third shape was reachable until the push before last: query.speculative-execution.policy
took both its numbers from the profile, so a reload could put a negative delay-ms — which
nonNegativeInteger rejects — into an otherwise valid document, or drop the group while the policy
still speculated. Both now come off the policy, whose constructor admits neither.

Fabricating an admissible value would misreport a setting an operator may have chosen deliberately,
and dropping the whole report would punish every other group for one field.

Approximations, flagged not changed ⚠️

Field Reported as Why that is an approximation For
A1 load-balancing.policy.load-distribution always shuffle LWT / serial-consistency requests take newQueryPlanPreserveReplicas, which never shuffles — replica-set in schema terms — and default-lwt-request-routing-method ships as PRESERVE_REPLICA_ORDER. So every LWT statement on a default config is distributed the way the report says it is not. No single enum value is honest. schema owner
A2 load-balancing.policy.fallback-to-non-preferred-nodes max-nodes-per-remote-dc > 0 and a datacenter preference exists Both terms are now required, which was the fix — and the second is not a Java-local judgement: @dkropachev settled the same question on the csharp sibling for its DC-agnostic RoundRobinPolicy, "for rr, there is no remote nodes or nodes outside of the node preferences, so having it true will be confusing, and yes, having it as false will be less confusing, not having it at all would be better, but there is no good way to do that" (the tail of that is now a schema follow-up). One term is still missing. maybeAddDcFailover also consults isDcFailoverAllowedForRequest, false for a DC-local consistency while allow-for-local-consistency-levels is off — and both of those ship as the default, so on a config that changes nothing but max-nodes-per-remote-dc the report says true while no ordinary statement fails over. Note the "it's per-request, a statement can override it" argument does not carry on its own: query.defaults.consistency is published under the same caveat. The real cost is that closing it needs ConsistencyLevelRegistry resolution of a string this report deliberately passes through unvalidated. A schema value meaning "conditional" is the honest fix. java, deliberate
A3 connection.socket.keep-alive, .reuse-address false when unset The driver never touches either socket option unless configured, so the effective value is the platform's — which is what the schema asks for, and which StandardSocketOptions documents as system dependent. false holds for JDK NIO on Linux; unverified for the native transports. Both keys are required, so omission is not available. Narrower than it looks beside #263, where @dkropachev found csharp's ReuseAddress was never wired to SO_REUSEADDR at all: DefaultNettyOptions does set both ChannelOptions whenever the option is defined, so only the unset case is approximated here. java
A4 control-plane.queries.system.timeout.client-side-ms CONTROL_CONNECTION_TIMEOUT Schema queries' own client-side wait is METADATA_SCHEMA_REQUEST_TIMEOUT, so the two siblings do not describe the same query — an operator debugging a slow schema query reads the wrong number. Already on the thread with @dkropachev; the fix is a queries.schema sibling, blocked today by additionalProperties:false. schema owner
A5 node-preference datacenter / rack values blank treated as unset A configured "" is reported as no preference while OptionalLocalDcHelper / OptionalLocalRackHelper hand it to the policy as a set-but-unmatchable datacenter. No alternative: nonEmptyString leaves no way to report "", and type:"dc" with the key omitted is invalid too. A padded value is no longer normalizednonEmptyString is minLength: 1, so " dc1 " is valid to emit and trimming it hid the typo an operator opens this report to find (raised by @dkropachev). Normalizing the runtime helpers instead was declined: that changes routing, in a reporting PR. java
A6 query.load-balancing.node-preference type:"rack" whenever a DC and a rack are configured Rack awareness lives only in DefaultLoadBalancingPolicy; BasicLoadBalancingPolicy never reads localRack, and PRESERVE_REPLICA_ORDER ignores it as well. Kept — the value is configured, and hiding a real setting is the worse failure mode. java
A8 both node-preference slots, when a node-distance evaluator is configured a datacenter preference is reported and the evaluator is not basic.load-balancing-policy.evaluator.class is consulted by computeNodeDistance before the datacenter and its verdict returned directly, so it can leave an in-DC node IGNORED and without a pool. Nothing can be reported for it: the option names a user-supplied class, the driver ships no location-based evaluator to introspect, and node-location-preference has no slot for a class name. Raised by @dkropachev on the gocql sibling, where DataCenterHostFilter is introspectable. java
A7 both node-preference slots, for a custom load balancing policy a configured datacenter is reported whatever the policy is Both parents claim an effect only the built-ins produce: connection's says the DC decides which nodes hold a pool, which holds because BasicLoadBalancingPolicy#computeNodeDistance makes an out-of-DC node IGNORED; query.load-balancing's says it scopes routing. A custom policy computes distance itself and need not read local-datacenter or withLocalDatacenter at all. Kept on A6's grounds. Deliberately asymmetric with the no-DC case, where the group is omitted rather than reporting a dc-auto the SPI never promises: nothing is inferred on a custom policy's behalf, while what was configured is passed through. java, deliberate

Two cosmetic ones, noted for completeness: a negative schema.agreement.timeout-ms normalizes to 0
(same outcome as 0, one extra round trip, and the schema cannot say "negative"); and
connection.connect.timeout-ms is reported as a full long while DefaultNettyOptions narrows it
with intValue(), so a connect timeout past ~24.8 days wraps in Netty.

Follow-up ⏭️

For the schema owner — all for the document rather than here. core/src/test/resources/config/driver-config-report-v1.schema.json
is a byte-for-byte copy of the document's normative block, so every item below lands there first
and the vendored copy is resynced afterwards. Adding a key here to close a review comment would fork
the contract and leave this driver's conformance suite validating against a schema no other
implementation has.

  • The revision updated the schema block but not the prose: the per-driver mapping tables still
    describe fields the schema no longer has, and both sample payloads still show the pre-restructure
    flat envelope, so they fail validation against the document's own schema.
  • $id and version still say v1 / const: 1 although earlier revisions removed a required
    top-level group and renamed load-balancing fields. By the schema's own versioning rule that is a
    major bump; harmless while every implementation is unreleased, but a v1 consumer cannot tell the
    shapes apart.
  • No size limit is specified even though all three drivers now enforce 32KiB.
  • dc-auto carries the inferred value in plain local-dc while rack-auto uses an explicit
    inferred- prefix. Implemented as specified; the asymmetry is easy to misread.
  • node-location-preference has no "no preference" variant (raised by @dkropachev). Omitting the
    optional group is the schema-valid answer and is what this PR does, but a none type would say it
    positively.
  • query.defaults.consistency needs either a wider type or a documented rule for names outside its
    enum — the one conformance gap a running Java driver can still produce. (An earlier revision of
    this list also asked the spec to define consumer behaviour for a non-positive in-flight.max;
    withdrawn — see Spec conformance, no session can reach it.)
  • control-plane.queries.system.timeout groups client-side-ms and server-side-ms as two views of
    one timeout. For Java they are not — see A4; a queries.schema sibling would let each class of
    query carry an honest pair. Agreed on the thread, and note the two asks interact: once
    queries.schema exists, METADATA_SCHEMA_REQUEST_TIMEOUT belongs there rather than under
    queries.system, so the server-side-ms this branch ungated will migrate.
  • server-side-ms needs the "reports configuration intent" clause pool.shard-aware.enabled already
    carries, now that it is emitted on backends where no USING TIMEOUT clause is ever sent.
  • connection.pool should carry local.size and remote.size (requested by @dkropachev; both
    options are always configured and consumed by ChannelPool). Blocked here: $defs/connection-pool
    is additionalProperties: false and this branch ships the schema block verbatim. Two things to
    settle in the shape — whether a size of 0 is representable, since positiveInteger would
    reproduce the objection raised against the old desired-connections-count; and that
    ChannelPool.initialize() ceil-divides the configured size across shards, so local.size = 1 on a
    4-shard node opens four connections and the number Java reports is not the connection count.
  • speculative-execution.policy.percentile is exclusiveMinimum: 0, while 3.x's
    PercentileSpeculativeExecutionPolicy accepts 0.0 — so an accurate report of that configuration
    is out of schema (raised by @dkropachev on Client config reporting (3.x) — stage 2: full DRIVER_CONFIG report #974). Unreachable from Java 4.x, which has no percentile
    policy at all, but the schema is shared.
  • fallback-to-non-preferred-nodes should be optional, so that "there is no node preference,
    therefore no non-preferred nodes to leave" can be said by omission rather than by a false that
    reads like a disabled feature. This is the tail of @dkropachev's Bump ch.qos.logback:logback-classic from 1.2.3 to 1.2.13 #263 comment quoted in A2 —
    "not having it at all would be better, but there is no good way to do that" — and it retires
    half of A2.
  • standard-error-aware has no normative rule set: its whole description is "Standard
    error-aware retry policy." @dkropachev challenged the csharp mapping on rules the spec does not
    state (csharp's DefaultRetryPolicy never retries Unavailable). Java's does, so that
    objection does not transfer — but no implementation's mapping is checkable until the type says
    what it means.

Other:

  • The vendored schema may be one revision behind. This branch's copy is byte-identical to
    Client config reporting (3.x) — stage 2: full DRIVER_CONFIG report #974's and Bump ch.qos.logback:logback-classic from 1.2.3 to 1.2.13 #263's (39128 bytes each); stage 2: populate the DRIVER_CONFIG report gocql#987's differs in exactly two places —
    $defs/requests/required drops orphaned, and orphaned.max's description gains "Absent only
    when this bound is unknown, for example when the client never replaces a connection over
    accumulated orphans and so has no limit to report." So either the document moved past the
    revision these three track, or that copy was edited locally and gocql's conformance suite
    validates against a forked contract. Asked on that thread, unanswered. Deliberately not
    resolved here
    : the resource has to stay a byte-for-byte copy of the document's normative
    block, and either way the change is permissive — Java always has an orphan limit, so nothing
    this branch emits changes.
  • The 3.x port (Client config reporting (3.x) — stage 2: full DRIVER_CONFIG report #974, DRIVER-382) lags this branch by several schema revisions and needs the same
    restructure. The sub-millisecond reconnection floor does not carry over: 3.x's
    ConstantReconnectionPolicy holds a long delayMs, so there is no sub-millisecond value to
    truncate. Separate PR, separate branch. Traffic goes the other way too: the
    speculative-execution source-of-truth fix on this branch was raised there first, and 3.x additionally
    had to stop reporting both built-ins as custom, which this branch never did.
  • Two CodeRabbit flags asking to "restore opt-in" reporting are declined, not overlooked — the
    default-on flip is intentional; reasoning is on the threads.
  • DriverBlockHoundIntegrationIT is JDK 14+ only and was not run locally. With reporting on by
    default the report is built on a Netty event loop; reasoned safe (no SSL factory resolution or IO
    with the default config, and Jackson is in-memory), but worth watching in CI. The larger half of
    that risk is gone: the reporter is now resolved during session init, so the event loop is no longer
    the first thread to load it and Jackson.
  • The 3.x port needs the Jackson guard too, if 3.x makes Jackson excludable the same way. Not checked
    here.
  • One thing found while re-auditing and not changed: relative links in prose across
    manual/ render with a spurious # prefix (href="#../configuration/reference/") — a site-wide
    MyST artifact affecting pre-existing links too, so it wants its own issue rather than a partial fix
    here. The one link this PR would have added was dropped for that reason.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The driver now reports expanded default-profile configuration through control-connection STARTUP options. ProtocolInitHandler derives ScyllaDB status from negotiated sharding information. SESSION_ID remains stable for a session and is sent on every connection. Tests validate the stage-2 payload against a version 1 schema.

Sequence Diagram(s)

sequenceDiagram
  participant StartupOptionsBuilder
  participant ProtocolInitHandler
  participant FeatureStore
  participant DriverConfigReporter
  StartupOptionsBuilder->>ProtocolInitHandler: provide stable SESSION_ID
  ProtocolInitHandler->>FeatureStore: read sharding information
  ProtocolInitHandler->>DriverConfigReporter: build control-connection DRIVER_CONFIG
Loading

Possibly related PRs

Suggested labels: P1, area/Driver_-_java-driver-4.x

Suggested reviewers: dkropachev, sylwiaszunejko

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The description references related follow-up work and issue numbers that align with the reporting and schema changes.
Out of Scope Changes check ✅ Passed The supporting API, protocol, schema, test, and documentation changes directly support full DRIVER_CONFIG reporting.
Title check ✅ Passed The title clearly identifies the main change: implementing the full DRIVER_CONFIG report for stage 2 of driver configuration reporting.
Description check ✅ Passed The description directly explains the full DRIVER_CONFIG report, its behavior, design decisions, validation, and test coverage.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks

Comment @coderabbitai help to get the list of available commands.

@nikagra
nikagra force-pushed the feature/driver-config-reporting-phase2 branch 2 times, most recently from ffe0609 to 9a2f6ca Compare July 29, 2026 12:04
@nikagra nikagra changed the title Client config reporting — stage 2: full DRIVER_CONFIG report Driver config reporting — stage 2: full DRIVER_CONFIG report Jul 29, 2026
@nikagra
nikagra force-pushed the feature/driver-config-reporting-phase2 branch from 9a2f6ca to c6f7ca3 Compare July 29, 2026 14:32
@nikagra
nikagra marked this pull request as ready for review July 29, 2026 15:18
@nikagra
nikagra requested a review from dkropachev July 29, 2026 15:18
@nikagra
nikagra requested a review from sylwiaszunejko July 29, 2026 15:23

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java`:
- Around line 1178-1187: Make the reporting documentation backend-neutral across
DefaultDriverOption, TypedDriverOption, and reference.conf: replace
ScyllaDB-only wording with server-side terminology or explicitly document both
storage paths, system.clients for ScyllaDB and system_views.clients for
Cassandra 4.1. Update all three affected sites consistently without changing the
reporting behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ab5c16aa-71fa-4f9a-9659-654b6920ae50

📥 Commits

Reviewing files that changed from the base of the PR and between 088290a and c6f7ca3.

📒 Files selected for processing (12)
  • core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java
  • core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporter.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/context/DriverConfigReporter.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/CassandraSchemaQueries.java
  • core/src/main/resources/reference.conf
  • core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryTestBase.java
  • core/src/test/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandlerTest.java
  • core/src/test/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporterTest.java
  • integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingCcmIT.java
  • integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingSimulacronIT.java

@nikagra
nikagra force-pushed the feature/driver-config-reporting-phase2 branch from c6f7ca3 to 24062e9 Compare July 30, 2026 12:13

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
core/src/main/java/com/datastax/oss/driver/internal/core/context/DriverConfigReporter.java (1)

51-57: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Contract extension is consistent with implementation and callers.

The new scyllaDb param and its "only meaningful with reportDriverConfig" contract match DefaultDriverConfigReporter.populateStartupOptions and ProtocolInitHandler's caller.

One minor note for the future: this interface now has two adjacent boolean parameters (reportDriverConfig, scyllaDb), which is a classic call-site readability/mix-up risk (e.g. populateStartupOptions(opts, true, false) reads ambiguously without named-parameter comments, as seen in the test file). Not blocking, but if a third flag is ever added, consider a small options value object instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/main/java/com/datastax/oss/driver/internal/core/context/DriverConfigReporter.java`
around lines 51 - 57, The comment identifies no required code change; the
current scyllaDb parameter and contract are consistent with the implementation
and callers. Leave DriverConfigReporter.populateStartupOptions and its call
sites unchanged, and only consider introducing an options value object if
another boolean flag is added later.
core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java (1)

194-210: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Centralize the ScyllaDB predicate. Both this startup path and CassandraSchemaQueries.shouldApplyUsingTimeout() key off the same shardingInfo != null signal; a shared helper would keep control-plane reporting and schema-query behavior in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java`
around lines 194 - 210, Centralize the ScyllaDB detection based on
getShardingInfo() != null in a shared helper, then update ProtocolInitHandler’s
startup reporting and CassandraSchemaQueries.shouldApplyUsingTimeout() to use
it. Preserve the existing featureStore population flow and behavior while
ensuring both paths rely on the same predicate.
core/src/test/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporterTest.java (1)

136-145: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

control no longer exercises the control-connection path.

Both calls pass reportDriverConfig=false, so the map named control is identical to pool. Passing true for the control map keeps the test name honest and additionally proves the session id is stable when the config blob is built.

♻️ Suggested tweak
-    reporter.populateStartupOptions(control, false, false);
+    reporter.populateStartupOptions(control, /* reportDriverConfig= */ true, false);
     reporter.populateStartupOptions(pool, false, false);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/test/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporterTest.java`
around lines 136 - 145, Update should_use_a_stable_session_id_across_connections
so the control map calls reporter.populateStartupOptions with
reportDriverConfig=true, while keeping the pool call false and preserving the
session ID equality assertion.
integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingCcmIT.java (1)

145-169: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stage-2 payload assertion is copy-pasted across both integration tests. Both classes carry an identical assertDriverConfigPayload (same Javadoc, same checks); every future stage-2 assertion has to be added twice and will silently drift otherwise.

  • integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingCcmIT.java#L145-L169: move this helper into a shared test utility (e.g. a package-private DriverConfigReportAssertions class in this package) and call it from here.
  • integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingSimulacronIT.java#L125-L149: delete the local copy and call the shared helper instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingCcmIT.java`
around lines 145 - 169, Extract the duplicated assertDriverConfigPayload helper
into a package-private shared DriverConfigReportAssertions test utility,
preserving its existing JSON parsing and stage-2 validation checks. In
integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingCcmIT.java
lines 145-169, replace the local helper with a call to the shared utility; in
integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingSimulacronIT.java
lines 125-149, delete the local copy and call the same utility.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java`:
- Around line 194-210: Centralize the ScyllaDB detection based on
getShardingInfo() != null in a shared helper, then update ProtocolInitHandler’s
startup reporting and CassandraSchemaQueries.shouldApplyUsingTimeout() to use
it. Preserve the existing featureStore population flow and behavior while
ensuring both paths rely on the same predicate.

In
`@core/src/main/java/com/datastax/oss/driver/internal/core/context/DriverConfigReporter.java`:
- Around line 51-57: The comment identifies no required code change; the current
scyllaDb parameter and contract are consistent with the implementation and
callers. Leave DriverConfigReporter.populateStartupOptions and its call sites
unchanged, and only consider introducing an options value object if another
boolean flag is added later.

In
`@core/src/test/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporterTest.java`:
- Around line 136-145: Update should_use_a_stable_session_id_across_connections
so the control map calls reporter.populateStartupOptions with
reportDriverConfig=true, while keeping the pool call false and preserving the
session ID equality assertion.

In
`@integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingCcmIT.java`:
- Around line 145-169: Extract the duplicated assertDriverConfigPayload helper
into a package-private shared DriverConfigReportAssertions test utility,
preserving its existing JSON parsing and stage-2 validation checks. In
integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingCcmIT.java
lines 145-169, replace the local helper with a call to the shared utility; in
integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingSimulacronIT.java
lines 125-149, delete the local copy and call the same utility.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5db2451e-3027-4740-bf80-230694676062

📥 Commits

Reviewing files that changed from the base of the PR and between c6f7ca3 and 24062e9.

📒 Files selected for processing (15)
  • core/pom.xml
  • core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java
  • core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporter.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/context/DriverConfigReporter.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/CassandraSchemaQueries.java
  • core/src/main/resources/reference.conf
  • core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryTestBase.java
  • core/src/test/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandlerTest.java
  • core/src/test/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporterTest.java
  • core/src/test/resources/config/driver-config-report-v1.schema.json
  • integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingCcmIT.java
  • integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingSimulacronIT.java
  • pom.xml
🚧 Files skipped from review as they are similar to previous changes (5)
  • core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryTestBase.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/CassandraSchemaQueries.java
  • core/src/main/resources/reference.conf
  • core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java
  • core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java

@nikagra
nikagra force-pushed the feature/driver-config-reporting-phase2 branch from 24062e9 to d6ac737 Compare July 30, 2026 13:40
@nikagra

nikagra commented Jul 30, 2026

Copy link
Copy Markdown
Author

@coderabbitai Thanks for the review — dispositions for the nitpicks, all pushed into the amended stage-2 commit:

  • D (test didn't exercise the control path): Fixed. should_use_a_stable_session_id_across_connections now passes reportDriverConfig=true for the control map, so it actually builds the DRIVER_CONFIG blob while asserting the session id is stable across connections.
  • E (duplicated assertDriverConfigPayload): Fixed. Extracted into a shared, package-private DriverConfigReportingAssertions in the same package; both DriverConfigReportingCcmIT and DriverConfigReportingSimulacronIT now call it.
  • B (two adjacent boolean params → options object): Leaving as-is for now, matching the note — the flags are documented at the interface and the call sites; worth revisiting with an options value object only if a third flag is ever added.
  • C (centralize the shardingInfo != null predicate): Skipping, deliberately. The two call sites reach sharding info through different objects and types — ProtocolInitHandler via ProtocolFeatureStore.getShardingInfo()ShardingInfo.ConnectionShardingInfo, and CassandraSchemaQueries.shouldApplyUsingTimeout() via DriverChannel.getShardingInfo()ShardingInfo. A shared predicate would be two trivial null-checks on unrelated types with no real logic to reuse, and both sites already cross-reference each other in comments.

Unrelated to the nitpicks but included in the same push: a TLS reporting correctness fix — tls.hostname-verification now reflects the SSL factory's real state via SslEngineFactory.requireHostnameValidation() rather than the advanced.ssl-engine-factory.hostname-validation option, which only governs the built-in DefaultSslEngineFactory (a factory supplied through SessionBuilder.withSslContext(...) would otherwise be mis-reported as validating).

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Relevant files =="
fd -t f . core/src/main integration-tests/src/test | rg 'DefaultDriverConfigReporter|DriverConfigReporting|SslEngineFactory|ProtocolInitHandler'

echo
echo "== Updated stable-session test and config reporting calls =="
rg -n -C 5 'should_use_a_stable_session_id_across_connections|populateStartupOptions\s*\(' \
  core/src/test/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporterTest.java \
  core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporter.java \
  core/src/main/java/com/datastax/oss/driver/internal/core/context/DriverConfigReporter.java \
  core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java

echo
echo "== Shared integration assertion helper and callers =="
rg -n -C 4 'class DriverConfigReportingAssertions|assertDriverConfigPayload\s*\(' \
  integration-tests/src/test/java/com/datastax/oss/driver/core/config

echo
echo "== TLS hostname-verification reporting and SSL factory contract =="
rg -n -C 5 'hostname-verification|requireHostnameValidation\s*\(' \
  core/src/main/java core/src/test/java

Length of output: 40821


@nikagra Thanks for the detailed dispositions. I verified the amended changes:

  • D: should_use_a_stable_session_id_across_connections now uses reportDriverConfig=true for the control connection, so the report-generation path is exercised.
  • E: DriverConfigReportingAssertions.assertDriverConfigPayload is shared package-private code and is called by both CCM and Simulacron integration tests.
  • B/C: Agreed—given the two well-documented flags and the distinct sharding-info types, neither abstraction adds enough value at present.
  • TLS: The correction is right: tls.hostname-verification now reflects SslEngineFactory.requireHostnameValidation(). The amended tests cover both a validating factory and a programmatic factory whose real state differs from the config option.

✏️ Learnings added
Learnt from: nikagra
URL: https://github.com/scylladb/java-driver/pull/968

Timestamp: 2026-07-30T13:44:06.515Z
Learning: In `core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporter.java`, `tls.hostname-verification` must describe the effective `SslEngineFactory.requireHostnameValidation()` state, not `advanced.ssl-engine-factory.hostname-validation`, because the latter only configures the built-in `DefaultSslEngineFactory` and does not describe factories supplied programmatically (for example with `SessionBuilder.withSslContext(...)`).

Learnt from: nikagra
URL: https://github.com/scylladb/java-driver/pull/968

Timestamp: 2026-07-30T13:44:06.515Z
Learning: The ScyllaDB detection null checks in `core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java` and `core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/CassandraSchemaQueries.java` intentionally remain local: they traverse distinct APIs and types (`ProtocolFeatureStore.getShardingInfo()` returning `ShardingInfo.ConnectionShardingInfo` versus `DriverChannel.getShardingInfo()` returning `ShardingInfo`), so a shared predicate would not centralize meaningful behavior.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingCcmIT.java (1)

129-138: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Prove that the reporting row is the control connection.

This verifies cardinality, not identity: a regression that emits DRIVER_CONFIG on one pool connection and omits it on the control connection still passes. Add a reliable control-connection marker/correlation before validating the payload, as the Simulacron test already does.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingCcmIT.java`
around lines 129 - 138, Update the reporting-row assertions in
DriverConfigReportingCcmIT to correlate the DRIVER_CONFIG row with the control
connection using the same reliable marker or correlation mechanism as the
existing Simulacron test. Validate that the identified row is the control
connection before calling assertDriverConfigPayload, while preserving the
existing single-row cardinality check.
core/src/test/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporterTest.java (1)

321-352: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider single-argument override helpers to cut the reporterWith(...) boilerplate.

The 7-arg reporterWith(defaults(map -> {}), mock(ExponentialReconnectionPolicy.class), mock(DefaultRetryPolicy.class), mock(NoSpeculativeExecutionPolicy.class), mock(DefaultLoadBalancingPolicy.class), mock(TimestampGenerator.class), Optional.empty()) call is repeated ~15 times across this file, varying in exactly one argument. Thin wrappers (or a small builder) would make each test's intent obvious.

♻️ Sketch
private DefaultDriverConfigReporter reporterWithReconnection(ReconnectionPolicy p) {
  return reporterWith(
      defaults(map -> {}),
      p,
      mock(DefaultRetryPolicy.class),
      mock(NoSpeculativeExecutionPolicy.class),
      mock(DefaultLoadBalancingPolicy.class),
      mock(TimestampGenerator.class),
      Optional.empty());
}
// likewise reporterWithRetry / reporterWithSpecEx / reporterWithLb / reporterWithSsl
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/test/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporterTest.java`
around lines 321 - 352, Reduce repeated seven-argument setup in
DefaultDriverConfigReporterTest by adding thin single-argument reporterWith
helper methods for the varying policy/configuration dependencies, including
reconnection policy and the analogous retry, speculative execution,
load-balancing, and SSL cases. Update the affected tests, such as
should_report_constant_reconnection_policy and
should_report_custom_reconnection_policy, to use the appropriate helper while
preserving their existing mocks and assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/src/test/resources/config/driver-config-report-v1.schema.json`:
- Around line 779-801: Update the consistency enum in the schema near the
consistency and serial-consistency properties to accept SERIAL and LOCAL_SERIAL
alongside the existing request consistency values. Keep the serial-consistency
property unchanged.

In
`@integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingAssertions.java`:
- Around line 29-43: Configure the shared OBJECT_MAPPER used by
assertDriverConfigPayload to enable
DeserializationFeature.FAIL_ON_TRAILING_TOKENS, ensuring readTree rejects valid
JSON followed by extra tokens while preserving the existing payload assertions.

---

Nitpick comments:
In
`@core/src/test/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporterTest.java`:
- Around line 321-352: Reduce repeated seven-argument setup in
DefaultDriverConfigReporterTest by adding thin single-argument reporterWith
helper methods for the varying policy/configuration dependencies, including
reconnection policy and the analogous retry, speculative execution,
load-balancing, and SSL cases. Update the affected tests, such as
should_report_constant_reconnection_policy and
should_report_custom_reconnection_policy, to use the appropriate helper while
preserving their existing mocks and assertions.

In
`@integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingCcmIT.java`:
- Around line 129-138: Update the reporting-row assertions in
DriverConfigReportingCcmIT to correlate the DRIVER_CONFIG row with the control
connection using the same reliable marker or correlation mechanism as the
existing Simulacron test. Validate that the identified row is the control
connection before calling assertDriverConfigPayload, while preserving the
existing single-row cardinality check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 74f644f4-90dd-478c-b6ac-fecd6b72bac0

📥 Commits

Reviewing files that changed from the base of the PR and between 24062e9 and d6ac737.

📒 Files selected for processing (20)
  • core/pom.xml
  • core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java
  • core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java
  • core/src/main/java/com/datastax/oss/driver/api/core/ssl/ProgrammaticSslEngineFactory.java
  • core/src/main/java/com/datastax/oss/driver/api/core/ssl/SslEngineFactory.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporter.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/context/DriverConfigReporter.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/CassandraSchemaQueries.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/ssl/DefaultSslEngineFactory.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/ssl/SniSslEngineFactory.java
  • core/src/main/resources/reference.conf
  • core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryTestBase.java
  • core/src/test/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandlerTest.java
  • core/src/test/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporterTest.java
  • core/src/test/resources/config/driver-config-report-v1.schema.json
  • integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingAssertions.java
  • integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingCcmIT.java
  • integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingSimulacronIT.java
  • pom.xml
🚧 Files skipped from review as they are similar to previous changes (4)
  • core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/CassandraSchemaQueries.java
  • core/src/main/resources/reference.conf
  • core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java

Comment thread core/src/test/resources/config/driver-config-report-v1.schema.json
@nikagra
nikagra force-pushed the feature/driver-config-reporting-phase2 branch from d6ac737 to 5e69715 Compare July 30, 2026 14:22

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java (1)

194-203: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Centralize the ScyllaDB detection check

getShardingInfo() != null is used here and again in CassandraSchemaQueries.shouldApplyUsingTimeout(). A shared helper would keep driver-config reporting and USING TIMEOUT gating aligned if the detection logic changes later.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java`
around lines 194 - 203, Centralize the ScyllaDB detection currently implemented
by getShardingInfo() != null into a shared helper, then update the
ProtocolInitHandler flow and CassandraSchemaQueries.shouldApplyUsingTimeout() to
use it. Preserve the existing featureStore null handling and ensure both
driver-config reporting and USING TIMEOUT gating rely on the same detection
logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java`:
- Around line 194-203: Centralize the ScyllaDB detection currently implemented
by getShardingInfo() != null into a shared helper, then update the
ProtocolInitHandler flow and CassandraSchemaQueries.shouldApplyUsingTimeout() to
use it. Preserve the existing featureStore null handling and ensure both
driver-config reporting and USING TIMEOUT gating rely on the same detection
logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: af57b7b6-6e4e-44fd-9609-e06c113aa08b

📥 Commits

Reviewing files that changed from the base of the PR and between d6ac737 and 5e69715.

📒 Files selected for processing (20)
  • core/pom.xml
  • core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java
  • core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java
  • core/src/main/java/com/datastax/oss/driver/api/core/ssl/ProgrammaticSslEngineFactory.java
  • core/src/main/java/com/datastax/oss/driver/api/core/ssl/SslEngineFactory.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporter.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/context/DriverConfigReporter.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/CassandraSchemaQueries.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/ssl/DefaultSslEngineFactory.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/ssl/SniSslEngineFactory.java
  • core/src/main/resources/reference.conf
  • core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryTestBase.java
  • core/src/test/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandlerTest.java
  • core/src/test/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporterTest.java
  • core/src/test/resources/config/driver-config-report-v1.schema.json
  • integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingAssertions.java
  • integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingCcmIT.java
  • integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingSimulacronIT.java
  • pom.xml
🚧 Files skipped from review as they are similar to previous changes (4)
  • core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/CassandraSchemaQueries.java
  • core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java
  • core/src/main/resources/reference.conf

nikagra added a commit to nikagra/java-driver that referenced this pull request Jul 30, 2026
Fills in the full DRIVER_CONFIG JSON report in the approved v2
cross-driver schema shape, replacing the stage-1 {"version":1}
placeholder. All groups are populated from Configuration and
Policies on each control-connection init.

Adds public getters to DCAwareRoundRobinPolicy and
RackAwareRoundRobinPolicy needed to report node-location-preference
and dc-failover, and makes PagingOptimizingLoadBalancingPolicy
implement ChainableLoadBalancingPolicy so the reporter can unwrap the
LB policy Cluster.Manager wraps at runtime.

Adds a JSON-Schema conformance test suite (mirroring the 4.x sibling
PR scylladb#968): the normative schema is shipped as a test resource and
validated via com.networknt:json-schema-validator (pinned to 1.5.x,
the last line still targeting Java 8), covering every discriminated-
union branch and optional group the 3.x reporter can emit, plus a
negative test proving additionalProperties=false is enforced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
nikagra added a commit to nikagra/java-driver that referenced this pull request Jul 30, 2026
Fills in the full DRIVER_CONFIG JSON report in the approved v2
cross-driver schema shape, replacing the stage-1 {"version":1}
placeholder. All groups are populated from Configuration and
Policies on each control-connection init.

Adds public getters to DCAwareRoundRobinPolicy and
RackAwareRoundRobinPolicy needed to report node-location-preference
and dc-failover, and makes PagingOptimizingLoadBalancingPolicy
implement ChainableLoadBalancingPolicy so the reporter can unwrap the
LB policy Cluster.Manager wraps at runtime.

Adds a JSON-Schema conformance test suite (mirroring the 4.x sibling
PR scylladb#968): the normative schema is shipped as a test resource and
validated via com.networknt:json-schema-validator (pinned to 1.5.x,
the last line still targeting Java 8), covering every discriminated-
union branch and optional group the 3.x reporter can emit, plus a
negative test proving additionalProperties=false is enforced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
nikagra added a commit to nikagra/java-driver that referenced this pull request Jul 30, 2026
Fills in the full DRIVER_CONFIG JSON report in the approved v2
cross-driver schema shape, replacing the stage-1 {"version":1}
placeholder. All groups are populated from Configuration and
Policies on each control-connection init.

Adds public getters to DCAwareRoundRobinPolicy and
RackAwareRoundRobinPolicy needed to report node-location-preference
and dc-failover, and makes PagingOptimizingLoadBalancingPolicy
implement ChainableLoadBalancingPolicy so the reporter can unwrap the
LB policy Cluster.Manager wraps at runtime.

Adds a JSON-Schema conformance test suite (mirroring the 4.x sibling
PR scylladb#968): the normative schema is shipped as a test resource and
validated via com.networknt:json-schema-validator (pinned to 1.5.x,
the last line still targeting Java 8), covering every discriminated-
union branch and optional group the 3.x reporter can emit, plus a
negative test proving additionalProperties=false is enforced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
nikagra added a commit to nikagra/java-driver that referenced this pull request Jul 31, 2026
Fills in the full DRIVER_CONFIG JSON report in the approved v2
cross-driver schema shape, replacing the stage-1 {"version":1}
placeholder. All groups are populated from Configuration and Policies
when the report is built, i.e. once per Cluster as it initializes.

Adds public getters to DCAwareRoundRobinPolicy and
RackAwareRoundRobinPolicy needed to report node-location-preference
and dc-failover, and makes PagingOptimizingLoadBalancingPolicy
implement ChainableLoadBalancingPolicy so the reporter can unwrap the
LB policy Cluster.Manager wraps at runtime.

Adds a JSON-Schema conformance test suite (mirroring the 4.x sibling
PR scylladb#968): the normative schema is shipped as a test resource and
validated via com.networknt:json-schema-validator (pinned to 1.5.x,
the last line still targeting Java 8), covering every discriminated-
union branch and optional group the 3.x reporter can emit, plus a
negative test proving additionalProperties=false is enforced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nikagra
nikagra force-pushed the feature/driver-config-reporting-phase2 branch from 5e69715 to cfda714 Compare July 31, 2026 17:13

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
core/src/test/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporterTest.java (1)

930-966: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider a small builder for the reporter fixtures.

The 7-argument and 8-argument reporterWith calls repeat across about twenty tests, and each call varies only one argument. A builder that starts from the default policy set and overrides one collaborator would remove that repetition and make each test state its single variable.

Example shape:

private final class ReporterBuilder {
  private DriverExecutionProfile profile = defaults(map -> {});
  private ReconnectionPolicy reconnection = mock(ExponentialReconnectionPolicy.class);
  // ... remaining collaborators with the same defaults as defaultsReporter()
  ReporterBuilder reconnection(ReconnectionPolicy p) { this.reconnection = p; return this; }
  DefaultDriverConfigReporter build() { /* wire the mock context */ }
}

Each test then reads builder().reconnection(mock(ConstantReconnectionPolicy.class)).build().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/test/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporterTest.java`
around lines 930 - 966, Refactor the repeated reporter fixture setup around the
overloaded reporterWith methods into a small ReporterBuilder that initializes
the same defaults as defaultsReporter() and exposes fluent overrides for
individual collaborators, including the programmatic local datacenter. Update
the affected tests to build reporters by overriding only the variable under
test, while preserving the existing mock context wiring and behavior.
core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporter.java (1)

152-152: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider package-private visibility for buildJson.

DefaultDriverConfigReporterTest is in the same package, com.datastax.oss.driver.internal.core.context. Package-private visibility therefore supports the test override without adding a subclass extension point that the javadoc must then qualify with thread-safety caveats.

♻️ Proposed change
-  protected String buildJson(boolean scyllaDb) {
+  String buildJson(boolean scyllaDb) {

If a production subclass hook is intended, keep protected and disregard this suggestion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporter.java`
at line 152, Change the buildJson method in DefaultDriverConfigReporter from
protected to package-private visibility, allowing
DefaultDriverConfigReporterTest to override it within the same package without
exposing a production subclass extension point.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/src/main/java/com/datastax/oss/driver/api/core/config/OptionsMap.java`:
- Line 403: Restore opt-in driver configuration reporting by setting
TypedDriverOption.DRIVER_CONFIG_REPORTING_ENABLED to false in OptionsMap. Update
DriverConfigReportingSimulacronIT at lines 53-60 and 145-160 to assert and
enable reporting explicitly as needed, and update upgrade_guide/README.md lines
40-44 to document that reporting is disabled by default.

In
`@core/src/main/java/com/datastax/oss/driver/internal/core/context/DriverConfigReporter.java`:
- Around line 23-33: Change driver-config reporting defaults from enabled to
disabled across OptionsMap.fillWithDriverDefaults and
DefaultDriverConfigReporter, then update the corresponding documentation and
tests to reflect false as the default. Preserve explicit opt-in behavior,
ensuring default sessions do not send the DRIVER_CONFIG startup option.

In
`@integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingCcmIT.java`:
- Around line 133-142: Update the assertions in DriverConfigReportingCcmIT to
identify the DRIVER_CONFIG row by matching its connection local address and port
against the control connection, using the existing control-connection details
and clientOptions helpers. Preserve the single-row and payload assertions, but
ensure a pooled connection cannot satisfy the test.

In `@upgrade_guide/README.md`:
- Around line 42-44: Update the fenced configuration block in the README to
specify the HOCON language identifier, changing the opening fence to use hocon
while preserving the existing configuration content.

---

Nitpick comments:
In
`@core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporter.java`:
- Line 152: Change the buildJson method in DefaultDriverConfigReporter from
protected to package-private visibility, allowing
DefaultDriverConfigReporterTest to override it within the same package without
exposing a production subclass extension point.

In
`@core/src/test/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporterTest.java`:
- Around line 930-966: Refactor the repeated reporter fixture setup around the
overloaded reporterWith methods into a small ReporterBuilder that initializes
the same defaults as defaultsReporter() and exposes fluent overrides for
individual collaborators, including the programmatic local datacenter. Update
the affected tests to build reporters by overriding only the variable under
test, while preserving the existing mock context wiring and behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e40d8974-45a6-4285-bc5b-2d6d30c334dc

📥 Commits

Reviewing files that changed from the base of the PR and between 5e69715 and cfda714.

📒 Files selected for processing (26)
  • core/pom.xml
  • core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java
  • core/src/main/java/com/datastax/oss/driver/api/core/config/OptionsMap.java
  • core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java
  • core/src/main/java/com/datastax/oss/driver/api/core/ssl/ProgrammaticSslEngineFactory.java
  • core/src/main/java/com/datastax/oss/driver/api/core/ssl/SslEngineFactory.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporter.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverContext.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/context/DriverConfigReporter.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/context/StartupOptionsBuilder.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/CassandraSchemaQueries.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/ssl/DefaultSslEngineFactory.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/ssl/SniSslEngineFactory.java
  • core/src/main/resources/reference.conf
  • core/src/test/java/com/datastax/dse/driver/internal/core/context/DseStartupOptionsBuilderTest.java
  • core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryTestBase.java
  • core/src/test/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandlerTest.java
  • core/src/test/java/com/datastax/oss/driver/internal/core/context/DefaultDriverConfigReporterTest.java
  • core/src/test/java/com/datastax/oss/driver/internal/core/context/StartupOptionsBuilderTest.java
  • core/src/test/resources/config/driver-config-report-v1.schema.json
  • integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingAssertions.java
  • integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingCcmIT.java
  • integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingSimulacronIT.java
  • pom.xml
  • upgrade_guide/README.md
🚧 Files skipped from review as they are similar to previous changes (13)
  • core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java
  • core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/ssl/DefaultSslEngineFactory.java
  • core/src/main/java/com/datastax/oss/driver/api/core/ssl/ProgrammaticSslEngineFactory.java
  • integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverConfigReportingAssertions.java
  • core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/CassandraSchemaQueries.java
  • core/pom.xml
  • core/src/main/java/com/datastax/oss/driver/internal/core/ssl/SniSslEngineFactory.java
  • core/src/main/java/com/datastax/oss/driver/api/core/ssl/SslEngineFactory.java
  • core/src/test/resources/config/driver-config-report-v1.schema.json
  • pom.xml
  • core/src/main/resources/reference.conf
  • core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryTestBase.java

Comment thread upgrade_guide/README.md Outdated
@nikagra
nikagra force-pushed the feature/driver-config-reporting-phase2 branch from cfda714 to 312b522 Compare July 31, 2026 18:18
@nikagra

nikagra commented Jul 31, 2026

Copy link
Copy Markdown
Author

Dispositions for the two nitpicks:

  • buildJson → package-private: done. The test override lives in the same package, so nothing else was needed, and the thread-safety javadoc dropped its production-subclass caveat accordingly.
  • Builder for the reporter fixtures: skipping, matching the "⚖️ Poor tradeoff" label — the explicit reporterWith(...) calls keep each test's full collaborator set visible at the call site, which is worth more here than the deduplication.

@nikagra

nikagra commented Jul 31, 2026

Copy link
Copy Markdown
Author

On the two nitpicks from the last CodeRabbit pass: buildJson is package-private now (312b5226b7) — the only override is the test one, in the same package, so no subclass hook is exposed. The ReporterBuilder fixture is deliberately skipped: test-only readability across ~25 call sites, not worth rewriting the suite at this point in review.

nikagra and others added 19 commits August 10, 2026 19:14
The cross-driver design doc revised v1 in place while this PR was in review.
The change is breaking but not a version bump — `$id` and `version` both stay
at 1, which is only safe because v1 has not shipped in a release yet.

The flat envelope becomes three groups — `connection`, `control-plane` and
`query` — with `additionalProperties: false` at the root, so every former
top-level group moves under one of them:

    socket                       -> connection.socket (now required)
    reconnection-policy          -> connection.reconnection.policy
    tls                          -> connection.tls (optional)
    retry-policy                 -> query.retry.policy
    load-balancing-policy        -> query.load-balancing.policy
    speculative-execution-policy -> query.speculative-execution.policy
    query-defaults               -> query.defaults
    control-plane.system-queries.timeout
                                 -> control-plane.queries.system.timeout
    control-plane.schema-agreement.timeout-ms
                                 -> control-plane.schema.agreement.timeout-ms

Beyond the re-homing, four changes alter what is emitted:

* `tls.enabled` and `adaptive-ordering.enabled` are gone — presence of each
  group is what reports it as on, so both are omitted rather than emitted
  with a false flag. `adaptive-ordering` also now requires a non-empty
  signal list, which rules out the old empty-array form.
* `dc-failover` is renamed `fallback-to-non-preferred-nodes`.
* `query.defaults.request` is optional, so a disabled request timeout is
  reported by omission. That was one of the two fields with no schema-valid
  form; only `connection.requests.in-flight.max` is left.
* `node-location-preference` now has two homes, and they are filled
  differently. `computeNodeDistance` derives node distance from the local DC
  alone — a node outside it is IGNORED, and an IGNORED node gets no pool — so
  the datacenter genuinely scopes connections and goes under
  `connection.node-preference`. The rack never reaches that method; it only
  reorders replicas at the head of a query plan, so the full preference
  belongs under `query.load-balancing.node-preference` and the connection
  group carries the datacenter half alone.

Also addresses three review comments from @dkropachev:

* Hostname verification is read from the engine factory the active
  `JdkSslHandlerFactory` wraps, not from `getSslEngineFactory()`. The two can
  differ, and going through the context could be the first caller to resolve
  a `LazyReference` nothing uses — reading keystore files on a Netty event
  loop, and costing the whole report if it throws.
* Reconnection delays are read from the running policy instead of the
  profile, since both built-ins latch them at construction. This also makes
  the schema's new `max-ms >= base-ms` invariant hold for free.
* The local DC the policy has already inferred is now reported, via the
  schema's `dc-auto.local-dc` and `rack-auto.inferred-local-dc` slots. It is
  null on the first control connection and resolved on every reconnect,
  which is exactly the distinction those fields exist to draw.

The five JSON syntax errors in the doc's schema block (four stray commas and
two missing ones) are fixed in the shipped resource, which is otherwise a
verbatim copy so it stays auditable against the doc.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The cross-driver design doc revised v1 in place again while this PR was in
review. The delta from the previous revision is one optional key: `max-retries`
is now permitted on the `standard-error-aware`, `never`, `downgrading-consistency`
and `custom` retry-policy variants. `simple` already required it and
`fallthrough` deliberately has no such key.

Adding a key is backward-compatible per the spec's own evolution rule, so `$id`
and `version` both stay at 1. The shipped schema resource is again byte-identical
to the doc's schema block, which the doc revision also fixed the JSON syntax of
(those five errors were already corrected here).

Nothing new is emitted. The key reports a retry limit taken *from configuration*,
and Java has no such option — no `max-retries` equivalent exists in
`reference.conf`, `DefaultDriverOption` or `TypedDriverOption`, which is why the
doc's own per-driver mapping table lists it as n/a for java. What the two
built-ins have instead are per-error-type rules hardcoded in Java: a single
attempt for read timeouts, write timeouts and unavailable, but an unbounded walk
down the query plan for aborted requests and error responses. No single number
describes that, so reporting one would be worse than omitting it. A custom policy
cannot be introspected for a limit either.

The three retry-policy tests now pin that omission, so the new schema slot is not
later filled with a hardcoded count.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A field-by-field audit of the shipped v1 schema against the code that actually
consumes each option found 30 of the 34 emitted fields exact. Three were not,
and are fixed here; the other nine findings are approximations the schema shape
cannot avoid, or items for the schema owner, and are listed in the PR
description rather than acted on.

1. `InternalError` was caught around the whole report build. It is a
   `VirtualMachineError`, so that also swallowed one raised by anything else on
   the path -- config access, a user policy, Jackson -- rather than only the
   documented `getSimpleName()` JDK edge case it was added for. The top-level
   catch is now `RuntimeException`, and the `InternalError` catch sits next to
   the `getSimpleName()` call it guards, behind a package-private `simpleName`
   seam so the branch stays testable (no class a test can declare provokes the
   error). Raised in review.

2. `query.defaults.serial-consistency` was reported verbatim.
   `basic.request.serial-consistency` is an unvalidated string that nothing
   checks until the first conditional statement runs (`Conversions`), while the
   schema's enum admits only `SERIAL` and `LOCAL_SERIAL` -- so a session
   configured with anything else produced a document that fails validation as a
   whole. Unlike its *required* sibling `consistency`, this key is optional, so
   the reporter's own documented omission principle applies here and simply was
   not being followed. Now emitted only for the two schema members, with the
   class javadoc explaining why this one is not a third known gap.

3. `dc-auto` was fabricated for policies that never infer a datacenter. The
   preference was omitted only for the exact `BasicLoadBalancingPolicy`; every
   custom policy with no configured DC still reported `dc-auto`, which claims a
   datacenter *will* be settled on -- something `LoadBalancingPolicy` nowhere
   requires an implementation to do. Now reported when a DC is configured, or
   the policy has already resolved one (evidence, read through `instanceof`, so
   a subclass counts), or its exact class is one of the four built-ins known to
   infer; otherwise omitted from both parents, where the group is optional.
   This subsumes the old exclusion including its rack-only case: no built-in
   looks for a rack before it knows a datacenter. Raised in review.

Test suite goes from 96 to 101 cases in `DefaultDriverConfigReporterTest`: the
`getSimpleName()` fail-safe test is reworked to assert the binary-name fallback
rather than a dropped report, plus an anonymous-policy naming case, an
out-of-enum and a `LOCAL_SERIAL` serial-consistency case, a DC-agnostic custom
policy, and a non-inferring policy that has nonetheless resolved a DC. The
default-report test now also pins `serial-consistency`. Full `core` suite green
(3889 tests).

Deliberately not folded into the commits that introduce this code, unlike the
previous rounds: a review is pending, and rewriting five SHAs mid-pass would
throw away the reviewer's "what changed since I last looked" diff. The three
regions all originate in commit `f445494`, so they can be folded on request.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The normative document has been revised again. Four changes reach this
driver:

  - connection.requests.in-flight.max drops its 1..32767 range for the
    shared positiveInteger definition;
  - query.defaults.consistency gains SERIAL and LOCAL_SERIAL;
  - query.defaults.client-timestamps and tls.hostname-verification
    become optional, absent when the behavior is unknown;
  - query.retry forbids a backoff on a fallthrough policy, which is
    vacuous here since the reporter emits neither.

The first two close both documents this reporter knowingly emitted out
of schema. What is left of each is much narrower: in-flight.max must
still be positive and nothing in the driver enforces that, and a
consistency name outside the enum now needs a custom
ConsistencyLevelRegistry, since the built-in load balancing policies
reject anything the default registry does not know before a report is
ever built. The class javadoc and the two tests that pinned the old
bounds say so; a third test pins the serial levels as now valid.

The vendored resource is again byte-identical to the document's schema
block. Two description rewrites come with it, one of which fixes the
dangling ../../node-preferences pointer this branch reported upstream.

Also assert the absent retry backoff on the group rather than on the
policy node, which is where the schema puts it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The accessor this branch added returns a plain boolean, defaulting to
false, so a custom factory that does validate host names is reported as
one that does not. That default was chosen because the schema had no
way to say "unknown" — tls.hostname-verification was required. It is
optional now, and absent is defined to mean exactly that, so the
accessor can stop guessing: it returns an Optional, empty by default,
and the reporter omits the key rather than inventing a boolean for a
security control it cannot read.

The three built-in factories all know their own answer and keep
reporting it. The other unknown case is unchanged in substance and now
says so the same way: when the handler factory in force is not the
driver's own JdkSslHandlerFactory, host name validation is a property of
a JDK SSLEngine that is not on that path, so the key is omitted instead
of reported false.

The method is new in this branch and unreleased, so the signature change
breaks nothing; revapi diffs against the last published release, where
it does not exist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same defect as the SSL accessor, and the same fix. isClientSide()
defaulted to true, on the grounds that assigning timestamps client-side
is the interface's contract — but it is only the usual contract:
returning NO_DEFAULT_TIMESTAMP from next() and letting the coordinator
assign is documented and legal, and nothing short of calling next() can
detect it. So the default reported every custom generator as
client-side, which is the over-claim moving the check off the class was
meant to remove; it only moved.

query.defaults.client-timestamps is optional now, with absent defined as
unknown, so the accessor returns an Optional and defaults to empty. Both
monotonic built-ins always assign the timestamp themselves, so one
override on their shared base covers them, and the server-side one keeps
reporting false.

The test that pinned the server-side path relied on Mockito answering an
unstubbed boolean with false; it stubs explicitly now, since with an
Optional return that silence would have turned it into an omission test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Twelve of this class's twenty-five profile reads used the no-fallback
getters, which throw when an option is absent. One throw is caught by
the single top-level handler, so a config source that omitted any one of
them dropped all thirty-odd fields behind one warning — and the
seven reads that did pass a fallback looked arbitrary next to them.

They are not arbitrary any more, because the schema decides. Where the
field or its enclosing group is optional, the fallback is the same
"disabled" sentinel that already omits it, so an undefined option is
reported exactly the way a disabled one is and no new branch is needed:
an undefined page size reads as unbounded, an undefined timeout as off,
an undefined max-executions drops the speculative-execution group.
Where the field is required, omitting it would invalidate the document,
so the fallback is the value reference.conf documents.

That covers all twenty-five reads — nineteen with a fallback, six behind
an isDefined guard — and makes the invariant statable: no missing option
costs more than the field it describes. Two of the five required-field
fallbacks cannot fire anyway, since ChannelFactory and the built-in load
balancing policies read those options before any report is built; the
javadoc says which and why.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fallback-to-non-preferred-nodes was read off max-nodes-per-remote-dc
alone, but BasicLoadBalancingPolicy#maybeAddDcFailover appends remote
nodes to a query plan only when that option is positive AND the policy
has a local DC to treat as preferred. So a config that changed nothing
but the option reported failover as on for a session where no remote
node is ever appended — and the key is defined in terms of leaving the
node preference, which such a report does not even carry.

The second term is the predicate the reporter already computes for the
node-preference groups, non-null exactly when a DC is configured, has
already been resolved by the policy, or the policy is one of the four
built-ins known to infer one. Reused rather than restated, so no policy
logic is duplicated.

One term is still missing on purpose: maybeAddDcFailover also consults
isDcFailoverAllowedForRequest, which is false for a DC-local consistency
while allow-for-local-consistency-levels is off. That is a per-request
decision a statement can override, and re-deriving it in a diagnostic
would duplicate exactly what this commit avoids.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…bsent

The driver declares Jackson as a required dependency but documents that it
can be excluded when unused (manual/core/integration), and enforces that for
Insights by checking for it before building the lifecycle listener. Driver
config reporting is a third Jackson user, it ships enabled, and it runs on
the connection initialization path -- with no such check.

On a classpath without Jackson, merely linking DefaultDriverConfigReporter
raises NoClassDefFoundError. That is an Error rather than an exception, and
it is raised while resolving the class rather than from any method it
declares, so neither the reporter's own fail-safe nor ProtocolInitHandler
can contain it: every control connection fails, and the session cannot be
built at all. A documented, supported configuration went from "the report is
skipped" to "the driver does not work", which is the opposite of the
invariant the reporter is written around.

So pick the implementation up front, the way buildLifecycleListeners()
already does, and fall back to a no-op reporter that names no Jackson type
anywhere -- one reference would make loading it fail for exactly the
deployments it exists to serve. Logged unconditionally, unlike the Insights
equivalent: nobody opted in to reporting, so nobody would think to look for
a message saying it is off.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
DefaultSession's init eagerly forces every user-facing policy before opening
any connection, so that a bad configuration fails the session rather than
each connect. The config reporter was the one component the reporting path
touches that was left out, which had a second consequence: a Netty event
loop became the first thread to load DefaultDriverConfigReporter and, with
it, Jackson -- reading jars from an event loop, mid-Startup.

Adding it to that list costs nothing (the reporter only stores the context)
and makes the ordering the reporter's javadoc already relied on true by
construction rather than by coincidence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The class javadoc, the comment in requests(), and the test that pins the
behavior all said a non-positive max-requests-per-connection "starts a
session -- one that cannot acquire a stream id" and is then reported. It
does not start one:

  - a negative value makes StreamIdGenerator's BitSet throw while
    ChannelFactory is still building the channel;
  - zero leaves no stream id at all, so ChannelHandlerRequest fails the
    control connection's own OPTIONS on preAcquireId, before Startup is
    composed and long before anything asks for a report.

So of the two required fields the schema constrains more tightly than the
option behind them, only query.defaults.consistency is reachable through a
running driver -- and even that needs a custom ConsistencyLevelRegistry. The
same configuration would also drive orphaned.max negative, which is a second
reason to read this as one unreachable shape rather than one field's gap.

Behavior is unchanged: the value is still passed through, and still pinned,
so it stays defined if the driver ever stops failing that early. Only the
claims about it change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
F6 in the value audit covers a configured rack reported for a policy that
ignores it, and X3 covers not fabricating dc-auto for a policy that may never
infer one. Neither covers the case in between: a configured *datacenter*
reported for a custom load balancing policy.

Both parents claim an effect only the built-ins produce. connection's
node-preference says the datacenter decides which nodes hold a pool, which
holds because BasicLoadBalancingPolicy#computeNodeDistance makes an out-of-DC
node IGNORED; query.load-balancing's says it scopes routing. A custom
LoadBalancingPolicy computes distance itself and need not read
basic.load-balancing-policy.local-datacenter or withLocalDatacenter at all,
so it may honor neither.

Kept as-is, on the same grounds as F6 -- hiding a setting the operator really
did make is the worse failure mode -- but the asymmetry with X3 is worth
stating where the decision lives: nothing is inferred on a custom policy's
behalf, while what was configured is passed through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The conformance suite covers every branch of every discriminated union the
reporter can emit, except one: speculative-execution's custom variant. The
constant variant is validated, and the subclass-reported-as-custom test
asserts the shape but never runs it past the schema.

It passes as written -- {type, name} with additionalProperties: true is valid
-- so this closes coverage rather than fixing anything. Worth having because
the enclosing group is optional and behaves differently per branch: it is
dropped entirely for NoSpeculativeExecutionPolicy but kept here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
connectionsPerShard() factored out arithmetic that initialize() and resize()
each spelled out, which is a fine change but has nothing to do with driver
config reporting. Reverted to keep the branch to its subject.

ProtocolFeatureStore#getNodeShardingInfo and the DriverChannel simplification
stay: the reporter needs the former, and the latter is its call site.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tls() restated, at length, the argument buildJson()'s javadoc already makes
for reading the engine factory off the handler in force rather than through
the context. Replaced with a pointer, leaving the javadoc as the single home
for it and the method comment to explain only what it does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…g with

query.speculative-execution.policy read max-executions and delay-ms from the
default profile at report time, while the policy latched both into final fields
when the context built it -- and advanced.speculative-execution-policy is
documented as not modifiable at runtime. After a configuration reload the report
published numbers no request executes with: a max-executions lowered to 1
dropped the whole group, claiming no speculative execution while the policy
still fired three, and a negative delay put a value the schema's
nonNegativeInteger rejects into an otherwise valid document. A context that
overrides buildSpeculativeExecutionPolicies() reaches the same divergence with
no reload at all.

Both values now come off the running ConstantSpeculativeExecutionPolicy, the way
the reconnection policy already is, so that policy's own constructor validation
(max-executions >= 1, delay >= 0) keeps the report inside the schema's ranges by
construction.

Raised by @dkropachev on the 3.x port (scylladb#974), where the same two fields were
misreported as a custom policy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two values a load balancing policy captures at construction were still being
read from the profile at report time, so a configuration reload could put the
report and the running policy out of step:

* `adaptive-ordering` came from `LOAD_BALANCING_POLICY_SLOW_AVOIDANCE`, while
  `DefaultLoadBalancingPolicy` latches it into a final field. Reloading the
  option to false dropped the group while the policy kept reordering replicas;
  reloading it the other way claimed an ordering that was never applied.
* the first term of `fallback-to-non-preferred-nodes` came from
  `LOAD_BALANCING_DC_FAILOVER_MAX_NODES_PER_REMOTE_DC`, latched the same way by
  `BasicLoadBalancingPolicy`, so the report could claim failover for a policy
  built with none, or deny it for one that appends remote nodes on every plan.

Both now read the accessors on the running instance, which is what the report
claims to describe -- the same source-of-truth fix already applied to the
reconnection delays and the speculative-execution parameters. The class javadoc
listed these two as the standing exception; it no longer needs to.

Raised by @dkropachev for adaptive ordering. The DC-failover term is the same
defect one field over and is fixed alongside it, since leaving one of a named
pair is worse than fixing neither. The third condition on `maybeAddDcFailover`
is untouched -- that is a per-request decision, not a stale read.

Tests pin each value with the profile set one way and the policy the other. The
mocked policies now go through a helper that stubs what they latched, since a
bare mock reports every built-in as having neither.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`control-plane.queries.system.timeout.server-side-ms` was emitted only when the
control connection's sharding information said the peer was ScyllaDB, mirroring
`CassandraSchemaQueries.shouldApplyUsingTimeout()` -- the same check that decides
whether a `USING TIMEOUT` clause built from this option reaches the wire at all.
That made the field describe the effect rather than the setting.

Per @dkropachev, who owns the schema, it carries what is configured:
`advanced.metadata.schema.request-timeout` is known before the driver connects,
and `pool.shard-aware.enabled` in the same report already reads as intent, with
its schema description saying so outright. So the gate is gone and the value is
emitted whenever it is positive. The cost is that an operator on generic
Cassandra now reads a server-side timeout nothing will enforce -- worth the
matching "reports configuration intent" sentence in the schema, raised upstream.

That was the reporter's only backend-conditional value, so the `NodeShardingInfo`
argument existed for it alone: it is dropped from `DriverConfigReporter`, both
implementations, `buildJson()` and `ProtocolInitHandler`'s `STARTUP` case, along
with the cross-reference comment in `CassandraSchemaQueries`. The report now
depends on nothing the peer said.

The two `ProtocolInitHandlerTest` cases that asserted the sharding information
reached the reporter go with the argument; `ShardingInfoTest` still covers the
parsing itself. The CCM integration test loses its backend-conditional branch --
the payload now reads identically on both backends.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`trimmedToNull` stripped surrounding whitespace before reporting `local-dc` and
`local-rack`, so a configured `" dc1 "` was published as `dc1`. The helpers that
feed the running policy -- `OptionalLocalDcHelper`, `OptionalLocalRackHelper` --
do no such thing: they hand the string over verbatim and match it against a
node's datacenter with `Objects.equals`, so that configuration matches no node at
all. The report hid exactly the typo an operator opens it to find.

The justification for trimming did not survive being checked. `nonEmptyString` is
`{"type":"string","minLength":1}`, so the padded form is valid to emit as is; the
constraint only ever covered the *blank* case, where there is genuinely nothing
to report -- `""` is not a `nonEmptyString`, and a `type: "dc"` preference with
the key omitted is invalid too. So `blankToNull` passes the value through and
maps only a blank one to "no preference", which stays documented as lossy.

Raised by @dkropachev. His alternative -- normalizing the runtime helpers so that
`" dc1 "` matches -- is declined here: that changes routing for existing users,
which does not belong in a change to a diagnostic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nikagra
nikagra force-pushed the feature/driver-config-reporting-phase2 branch from 4ddf4ca to 7bfc53c Compare August 10, 2026 17:19
nikagra and others added 3 commits August 11, 2026 00:29
`nodeLocation()` read `local-datacenter` from the profile and consulted
`BasicLoadBalancingPolicy.getLocalDatacenter()` only when nothing was configured.
The context builds the load balancing policy once, in a `LazyReference`, so a
profile reloaded from dc1 to dc2 leaves the policy still treating dc1 as local --
an out-of-dc1 node stays IGNORED and gets no pool -- while both `node-preference`
groups published dc2. The report described a locality no request was routed by.

The resolved value now wins wherever the policy has one. Whether anything was
configured decides only which schema slot it occupies: a datacenter the policy
took from an earlier generation of the configuration is still explicitly
configured, merely stale, so it stays `type: "dc"` rather than being demoted to
`dc-auto`. Exactly one of the configured and inferred forms survives, which keeps
the schema's constraint on the pair true by construction.

Before initialization -- the state the very first control connection sends
STARTUP in -- nothing is resolved and the configured value stands alone,
unchanged, and a custom policy exposes no accessor so what it was configured with
is still passed through.

This also drops the re-derivation of `OptionalLocalDcHelper`'s precedence for
every state except the pre-init one, closing an item the design doc had carried
since the first draft as a duplication concern -- which is why it was never read
as the staleness bug it also was.

Found by sweeping every option the reporter reads against whether its consumer
stores the value, rather than by review; it is the seventh and last instance of
that defect in this class.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four review rounds produced findings that were nearly all instances of three
rules, and the rules were nowhere written down -- so each instance had to be
raised, argued and fixed on its own. They are now on the class, where someone
adding a field will see them:

* Source. Report the object in force, not the configuration it was built from. A
  policy or factory that captures an option once keeps using that value for the
  life of the session, since the context holds every one of them in a once-built
  LazyReference; the profile is then the wrong source. Six fields already read
  their instance for this reason and the paragraph now names all of them, along
  with the question to ask of a new one.
* Range. Every value the option legally accepts must land inside the schema's
  constraint or take the omission route -- disabled, negative, sub-millisecond
  against a whole-millisecond field, undefined. With the trap that produced the
  max-executions off-by-one: the driver's units and the schema's need not agree,
  so compare the definitions rather than the names.
* Warrant. Do not assert a property the implementation does not guarantee. This
  one is a judgement call, and the javadoc says so; what it can pin down is the
  line this class has consistently drawn -- nothing is inferred on a third
  party's behalf, but what was explicitly configured is passed through even where
  the component in force may ignore it.

Documentation only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both were asked on a sibling implementation of this same schema and neither
answer existed in 4.x, so both would have cost a review round to establish.

`max-retries` was asked for on the 3.x port as an unconditional 1, and declined
there. 4.x is identical and the existing note ("no single number describes that")
does not show why, since 1 looks correct: DefaultRetryPolicy really does cap
onReadTimeout, onWriteTimeout and onUnavailable at retryCount == 0. What it omits
is that CqlRequestHandler reaches onErrorResponseVerdict only for an idempotent
statement and then never checks the count, so the same policy bounds a
non-idempotent request at 1 and an idempotent one at the length of the query
plan. Idempotence is per statement, which a session-level report cannot know.

`connection.node-preference` was the only comment on the gocql PR, asking that it
be populated from introspectable location filters such as DataCenterHostFilter.
Java's analogue is basic.load-balancing-policy.evaluator.class, and it deserves a
note for a stronger reason than the absence of one: computeNodeDistance consults
the evaluator before the datacenter and returns its verdict directly, so it can
leave an in-DC node IGNORED and without a pool -- weakening the same claim the
group already qualifies for custom policies. Nothing is reported for it and
nothing can be: the option names a user-supplied class and the driver ships no
location-based evaluator of its own, so there is no datacenter to read out.

Documentation only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nikagra added a commit to nikagra/java-driver that referenced this pull request Aug 10, 2026
Fills in the full DRIVER_CONFIG JSON report in the normative cross-driver
schema shape, replacing the stage-1 {"version":1} placeholder. All groups
are populated from Configuration and Policies when the report is built,
i.e. once per Cluster as it initializes.

Everything hangs off three groups. connection carries the connect/read
timeouts, the per-connection request capacity, the pool, the socket
options, the reconnection policy and -- only when TLS is on -- tls.
control-plane carries the system-query and schema-agreement timeouts.
query carries the per-request defaults plus the three policies acting on
a query: retry, load-balancing (with the node preference beside it) and,
when configured, speculative-execution.

The schema reports the node preference in two places and 3.x fills both
from the same policy chain: query.load-balancing.node-preference for what
a query is routed by, connection.node-preference for the part of the
cluster the driver holds connections to. One LoadBalancingPolicy decides
both, since distance(Host) governs whether a host is pooled at all, so
the connection key carries the datacenter half alone. A rack-aware
policy's distance() returns REMOTE, never IGNORED, for a local-datacenter
host in another rack, so those hosts are still pooled and the rack scopes
no pooling at all; the datacenter does, a host outside the preferred one
being IGNORED unless the policy is configured to use hosts there, and an
ignored host gets no pool.

token-aware is the only built-in load balancing shape the schema defines,
so every other built-in policy -- a bare DCAwareRoundRobinPolicy,
RoundRobinPolicy, WhiteListPolicy -- is reported as custom with its class
name, which identifies it but carries none of the normalized flags; its
datacenter and rack still show up in query.load-balancing.node-preference.
A token-aware chain reports load-distribution from its replica ordering
(RANDOM, the 3.x default, is "shuffle"; TOPOLOGICAL is "replica-set";
NEUTRAL keeps the child's plan order, so "round-robin").

fallback-to-non-preferred-nodes is true whenever the policy can reach a
node outside the preference reported beside it. For DCAwareRoundRobin
that means used-hosts-per-remote-DC, since the preference is the
datacenter. RackAwareRoundRobin reports a rack, and the other racks of
its local datacenter are outside that yet are the second tier of every
query plan -- distance() returns REMOTE, not IGNORED, for them -- so it
is always true there, remote datacenter hosts or not.

adaptive-ordering has no "enabled" flag and cannot carry an empty signal
list, so it is reported only when a LatencyAwarePolicy is in the chain --
latency being the only runtime observation a 3.x policy can reorder
candidates on. tls likewise has no "enabled" flag: the group's presence
is what says TLS is on.

Where a configured value falls outside what the schema can express, an
optional key or group is omitted rather than emitted as a value the
schema rejects: a disabled connect timeout, a disabled read timeout (all
three of connection.read, control-plane.queries.system.timeout
.client-side-ms and query.defaults.request), a negative SO_LINGER, a
non-positive socket buffer size, an unbounded page size, and a default
serial consistency level that is not serial -- QueryOptions, unlike
Statement, does not check that one. Two optional bounds are omitted for
the opposite reason -- 3.x has no such bound to report at all:
connection.reconnection.policy.max-attempts, since its reconnection
policies retry forever (the maxAttempts field ExponentialReconnection
Policy carries is an overflow guard on the doubling, not a give-up
bound: past it nextDelayMs() keeps returning maxDelayMs), and
query.retry.policy.max-retries, since no
single number describes a 3.x retry policy. Both built-ins are
parameterless singletons, and while they stop after one attempt on a
read timeout, a write timeout or an unavailable error -- all three
sharing one counter, so one retry between them rather than one each --
onRequestError leaves nbRetry unread and keeps trying the next host
until the query plan runs out. Which of the two applies is decided per
statement rather than by configuration: RequestHandler only consults
onRequestError and onWriteTimeout for an idempotent statement, so the
same policy bounds a non-idempotent request at one retry and an
idempotent one at the length of the query plan, and setIdempotent
overrides the reported query.defaults.idempotence per statement.

Two more keys are omitted for a third reason -- the schema admits only a
boolean, and 3.x cannot observe which one applies.
query.defaults.client-timestamps is false for ServerSideTimestamp
Generator, whose next() always returns Long.MIN_VALUE, and true for an
AbstractMonotonicTimestampGenerator, which never can; any other
generator makes that a per-call decision, so whether timestamps are
assigned client-side is not a property of the configuration at all.
connection.tls.hostname-verification is true for SniSSLOptions, the
driver's only setEndpointIdentificationAlgorithm call, and omitted for
every other SSLOptions, which builds its engine from a user SSLContext
or hands the whole handler to Netty. Both keys are documented as absent
exactly when the behavior is unknown, which is this case. The tls group
can therefore be empty -- its presence is still what reports TLS is on.

Omission is not always available, so these required keys are left in the
one state that is accurate:

- connection.requests.orphaned.max has no 3.x equivalent to report at
  all. A request the driver stopped waiting for keeps its stream
  identifier until the response arrives, with no configurable bound and
  no connection replacement, so the key is omitted -- which its
  required-ness then rejects. This is the one violation every report
  carries.
- connection.requests.in-flight.max must be positive, while
  PoolingOptions also accepts 0. Only PoolingOptions.UNSET falls back to
  a protocol default, so a limit of 0 an operator set deliberately is
  not reported as 1024.
- query.speculative-execution.policy.percentile is bounded to 0..100
  exclusive, while PercentileSpeculativeExecutionPolicy accepts a
  percentile of 0.

Such a value is reported as-is and the limitation is documented on the
class: the reporter neither fabricates an in-range value -- which would
misreport a setting an operator may have chosen on purpose, or a policy
3.x does not implement -- nor drops the whole report over one field.
Recorded as a cross-driver schema gap, to be fixed the way
control-plane.schema.agreement.timeout-ms already admits 0.

QueryOptions.setConsistencyLevel now rejects null -- a behavior change
to a public setter. Every query needs a consistency level, so a null
default already failed any statement that did not set one of its own:
SessionManager falls back to it for every request, and
CBUtil.writeConsistencyLevel then dereferences it to write the frame.
That turned a schema-required key into a missing one for a
configuration that could never work. setSerialConsistencyLevel is
deliberately left as it is: the schema makes serial-consistency
optional, so a null there is faithfully reported as an omission rather
than as a missing required key. The reporter keeps omitting a
null it is handed anyway: the field is private, so only a QueryOptions
subclass overriding the getter can still produce one, and letting it
through would throw and cost the whole report rather than one key.

Adds the public getters the report needs: local DC/rack, their explicit
flags and used-hosts-per-remote-DC on DCAwareRoundRobinPolicy, the same
minus used-hosts-per-remote-DC on RackAwareRoundRobinPolicy, replica
ordering on TokenAwarePolicy, and max-executions plus the delay or
percentile on the two built-in speculative execution policies -- whose
parameters are immutable and land in the schema's range exactly, so
they are reported as constant/percentile rather than as custom. Also
makes PagingOptimizingLoadBalancingPolicy implement
ChainableLoadBalancingPolicy so the reporter can unwrap the LB policy
Cluster.Manager wraps at runtime.

in-flight.max needs a fallback because PoolingOptions is still UNSET
when the report is built: the protocol version is only negotiated once
the control connection is up. The default row is resolved with the same
walk PoolingOptions.setProtocolVersion applies -- the highest DEFAULTS
key not above the version -- driven by the version the user pinned with
withProtocolVersion when they pinned one, and by v3 otherwise, that
being the lowest version ScyllaDB negotiates and the reference row for
everything above it. DEFAULTS holds only v1 and v3 rows, so a cluster
pinned to v2 is sized from v1's 128 rather than v3's 1024, and pinning
is the one part of negotiation knowable at report time.

Caps the report at 32KiB of UTF-8 (MAX_DRIVER_CONFIG_LENGTH), matching
the 4.x sibling PR scylladb#968, gocql scylladb#964 and csharp-driver scylladb#262. Beyond
cross-driver parity this is a correctness fix: CBUtil.writeString
writes each STARTUP value with a 16-bit length prefix and no bounds
check, so a value over 65535 bytes truncates the prefix modulo 65536
while still appending the whole body -- a corrupt frame and a failed
handshake, and not something the fail-safe try/catch can contain since
nothing throws. Parts of the report are user-supplied and unbounded
(DC/rack names, consistency levels, custom policy class names). Over
the limit means WARN and no DRIVER_CONFIG.

Hardens the other two ways reporting could break a connection rather
than merely fail to report:

- The fail-safe catch also covers InternalError, since customPolicy()
  calls getClass().getSimpleName() on arbitrary user policy objects
  (documented JDK edge case for certain synthetic classes). Not a bare
  Error, so OutOfMemoryError/StackOverflowError still surface.
- The load balancing policy chain walk is bounded at 16 policies and
  shared by both callers. It follows getChildPolicy() on arbitrary user
  policies, so a cyclic chain used to spin forever on the Cluster
  initialization path -- the one failure mode the try/catch cannot
  contain, because it hangs rather than throws.

A custom load balancing policy is now named after the policy the user
configured rather than PagingOptimizingLoadBalancingPolicy. Cluster
.Manager wraps every session's policy in that internal class, and it is
the outermost element of the chain, so every custom policy was reported
as {"type":"custom","name":"PagingOptimizingLoadBalancingPolicy"}. An
anonymous policy class falls back to its binary name, since it has no
simple name and the schema requires a non-empty one.

Adds a JSON-Schema conformance test suite (mirroring the 4.x sibling
PR scylladb#968): the normative schema block is shipped verbatim as a test
resource -- design-doc revision v5, whose report version field is still
1 -- and validated via com.networknt:json-schema-validator (1.5.x,
the last line still targeting Java 8), covering every discriminated-
union branch and optional group the 3.x reporter can emit. Since one
required key has no value to report, the assertion is that a report
violates the schema in exactly the documented ways and no other, with
a test naming the gap and a negative test proving
additionalProperties=false is enforced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nikagra and others added 5 commits August 11, 2026 11:16
The two places an operator reads when deciding to turn reporting off -- the
reference.conf block and the option's own javadoc -- both stopped at "when
false, DRIVER_CONFIG is not sent". The asymmetry was written down only in
StartupOptionsBuilder.SESSION_ID_KEY, in DriverConfigReporter and in the
upgrade guide, none of which is where that decision gets made.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reading the datacenter and rack the policy resolved meant widening
BasicLoadBalancingPolicy#getLocalDatacenter and #getLocalRack from protected to
public. The load-balancing manual invites subclassing that class and overriding
"only the methods that you wish to modify", and Java does not let an override
reduce visibility -- so an existing subclass overriding either one no longer
compiles, though it still runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both are cases where the documentation admits more than what is reachable or
guaranteed.

The mixed node-preference variants read as though any combination arises today.
None does: BasicLoadBalancingPolicy#init discovers the rack through
OptionalLocalRackHelper, which reads configuration and never infers one, and
only once a datacenter is known -- so a resolved rack always implies a
configured one and inferredRack is null for every built-in. The inferred-rack
field serves a subclass overriding discoverLocalRack, which nodeLocation() does
read, and now says so.

The two new accessors said only that an implementation which knows should
override them. That misses the case the reporter is otherwise careful about
everywhere it checks an exact class: the built-ins' answers are inherited rather
than defaulted to empty, so a subclass that changes the behavior these describe
reports its parent's answer unless it overrides them too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both existing size-limit tests feed a synthetic string through the buildJson()
seam, so nothing showed that a report can reach 32KiB at all -- even though
unbounded user-supplied values are the whole reason the limit exists. The new
test sets a datacenter name half the limit long, which the report carries under
both node-preference parents, and pins that the result is a well-formed,
schema-valid document dropped for its size rather than a build that failed.

Same gap raised against the csharp sibling (scylladb/csharp-driver#263), where
the cap was likewise only ever exercised through a test subclass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The message this constructor rejects a negative base-delay with formats a
Duration with %d, so String.format raises IllegalFormatConversionException
instead -- the operator sees a format error rather than which option is wrong.

Pre-existing, and unrelated to configuration reporting beyond this constructor
being where the reported delay is now latched; kept as its own commit so it can
be dropped without touching the rest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nikagra added a commit to nikagra/java-driver that referenced this pull request Aug 11, 2026
Fills in the full DRIVER_CONFIG JSON report in the normative cross-driver
schema shape, replacing the stage-1 {"version":1} placeholder. All groups
are populated from Configuration and Policies when the report is built,
i.e. once per Cluster as it initializes.

Everything hangs off three groups. connection carries the connect/read
timeouts, the per-connection request capacity, the pool, the socket
options, the reconnection policy and -- only when TLS is on -- tls.
control-plane carries the system-query and schema-agreement timeouts.
query carries the per-request defaults plus the three policies acting on
a query: retry, load-balancing (with the node preference beside it) and,
when configured, speculative-execution.

The schema reports the node preference in two places and 3.x fills both
from the same policy chain: query.load-balancing.node-preference for what
a query is routed by, connection.node-preference for the part of the
cluster the driver holds connections to. One LoadBalancingPolicy decides
both, since distance(Host) governs whether a host is pooled at all, so
the connection key carries the datacenter half alone. A rack-aware
policy's distance() returns REMOTE, never IGNORED, for a local-datacenter
host in another rack, so those hosts are still pooled and the rack scopes
no pooling at all; the datacenter does, a host outside the preferred one
being IGNORED unless the policy is configured to use hosts there, and an
ignored host gets no pool.

token-aware is the only built-in load balancing shape the schema defines,
so every other built-in policy -- a bare DCAwareRoundRobinPolicy,
RoundRobinPolicy, WhiteListPolicy -- is reported as custom with its class
name, which identifies it but carries none of the normalized flags; its
datacenter and rack still show up in query.load-balancing.node-preference.
A token-aware chain reports load-distribution from its replica ordering
(RANDOM, the 3.x default, is "shuffle"; TOPOLOGICAL is "replica-set";
NEUTRAL keeps the child's plan order, so "round-robin").

fallback-to-non-preferred-nodes is true whenever the policy can reach a
node outside the preference reported beside it. For DCAwareRoundRobin
that means used-hosts-per-remote-DC, since the preference is the
datacenter. RackAwareRoundRobin reports a rack, and the other racks of
its local datacenter are outside that yet are the second tier of every
query plan -- distance() returns REMOTE, not IGNORED, for them -- so it
is always true there, remote datacenter hosts or not.

adaptive-ordering has no "enabled" flag and cannot carry an empty signal
list, so it is reported only when a LatencyAwarePolicy is in the chain --
latency being the only runtime observation a 3.x policy can reorder
candidates on. tls likewise has no "enabled" flag: the group's presence
is what says TLS is on.

Where a configured value falls outside what the schema can express, an
optional key or group is omitted rather than emitted as a value the
schema rejects: a disabled connect timeout, a disabled read timeout (all
three of connection.read, control-plane.queries.system.timeout
.client-side-ms and query.defaults.request), a negative SO_LINGER, a
non-positive socket buffer size, an unbounded page size, and a default
serial consistency level that is not serial -- QueryOptions, unlike
Statement, does not check that one. Two optional bounds are omitted for
the opposite reason -- 3.x has no such bound to report at all:
connection.reconnection.policy.max-attempts, since its reconnection
policies retry forever (the maxAttempts field ExponentialReconnection
Policy carries is an overflow guard on the doubling, not a give-up
bound: past it nextDelayMs() keeps returning maxDelayMs), and
query.retry.policy.max-retries, since no
single number describes a 3.x retry policy. Both built-ins are
parameterless singletons, and while they stop after one attempt on a
read timeout, a write timeout or an unavailable error -- all three
sharing one counter, so one retry between them rather than one each --
onRequestError leaves nbRetry unread and keeps trying the next host
until the query plan runs out. Which of the two applies is decided per
statement rather than by configuration: RequestHandler only consults
onRequestError and onWriteTimeout for an idempotent statement, so the
same policy bounds a non-idempotent request at one retry and an
idempotent one at the length of the query plan, and setIdempotent
overrides the reported query.defaults.idempotence per statement.

Two more keys are omitted for a third reason -- the schema admits only a
boolean, and 3.x cannot observe which one applies.
query.defaults.client-timestamps is false for ServerSideTimestamp
Generator, whose next() always returns Long.MIN_VALUE, and true for an
AbstractMonotonicTimestampGenerator, which never can; any other
generator makes that a per-call decision, so whether timestamps are
assigned client-side is not a property of the configuration at all.
connection.tls.hostname-verification is true for SniSSLOptions, the
driver's only setEndpointIdentificationAlgorithm call, and omitted for
every other SSLOptions, which builds its engine from a user SSLContext
or hands the whole handler to Netty. Both keys are documented as absent
exactly when the behavior is unknown, which is this case. The tls group
can therefore be empty -- its presence is still what reports TLS is on.

Omission is not always available, so these required keys are left in the
one state that is accurate:

- connection.requests.orphaned.max has no 3.x equivalent to report at
  all. A request the driver stopped waiting for keeps its stream
  identifier until the response arrives, with no configurable bound and
  no connection replacement, so the key is omitted -- which its
  required-ness then rejects. This is the one violation every report
  carries.
- connection.requests.in-flight.max must be positive, while
  PoolingOptions also accepts 0. Only PoolingOptions.UNSET falls back to
  a protocol default, so a limit of 0 an operator set deliberately is
  not reported as 1024.
- query.speculative-execution.policy.percentile is bounded to 0..100
  exclusive, while PercentileSpeculativeExecutionPolicy accepts a
  percentile of 0.

Such a value is reported as-is and the limitation is documented on the
class: the reporter neither fabricates an in-range value -- which would
misreport a setting an operator may have chosen on purpose, or a policy
3.x does not implement -- nor drops the whole report over one field.
Recorded as a cross-driver schema gap, to be fixed the way
control-plane.schema.agreement.timeout-ms already admits 0.

QueryOptions.setConsistencyLevel now rejects null -- a behavior change
to a public setter. Every query needs a consistency level, so a null
default already failed any statement that did not set one of its own:
SessionManager falls back to it for every request, and
CBUtil.writeConsistencyLevel then dereferences it to write the frame.
That turned a schema-required key into a missing one for a
configuration that could never work. setSerialConsistencyLevel is
deliberately left as it is: the schema makes serial-consistency
optional, so a null there is faithfully reported as an omission rather
than as a missing required key. The reporter keeps omitting a
null it is handed anyway: the field is private, so only a QueryOptions
subclass overriding the getter can still produce one, and letting it
through would throw and cost the whole report rather than one key.

Adds the public getters the report needs: local DC/rack, their explicit
flags and used-hosts-per-remote-DC on DCAwareRoundRobinPolicy, the same
minus used-hosts-per-remote-DC on RackAwareRoundRobinPolicy, replica
ordering on TokenAwarePolicy, and max-executions plus the delay or
percentile on the two built-in speculative execution policies -- whose
parameters are immutable and land in the schema's range exactly, so
they are reported as constant/percentile rather than as custom. Also
makes PagingOptimizingLoadBalancingPolicy implement
ChainableLoadBalancingPolicy so the reporter can unwrap the LB policy
Cluster.Manager wraps at runtime.

in-flight.max needs a fallback because PoolingOptions is still UNSET
when the report is built: the protocol version is only negotiated once
the control connection is up. The default row is resolved with the same
walk PoolingOptions.setProtocolVersion applies -- the highest DEFAULTS
key not above the version -- driven by the version the user pinned with
withProtocolVersion when they pinned one, and by v3 otherwise, that
being the lowest version ScyllaDB negotiates and the reference row for
everything above it. DEFAULTS holds only v1 and v3 rows, so a cluster
pinned to v2 is sized from v1's 128 rather than v3's 1024, and pinning
is the one part of negotiation knowable at report time.

Caps the report at 32KiB of UTF-8 (MAX_DRIVER_CONFIG_LENGTH), matching
the 4.x sibling PR scylladb#968, gocql scylladb#964 and csharp-driver scylladb#262. Beyond
cross-driver parity this is a correctness fix: CBUtil.writeString
writes each STARTUP value with a 16-bit length prefix and no bounds
check, so a value over 65535 bytes truncates the prefix modulo 65536
while still appending the whole body -- a corrupt frame and a failed
handshake, and not something the fail-safe try/catch can contain since
nothing throws. Parts of the report are user-supplied and unbounded
(DC/rack names, consistency levels, custom policy class names). Over
the limit means WARN and no DRIVER_CONFIG.

Hardens the other two ways reporting could break a connection rather
than merely fail to report:

- The fail-safe catch also covers InternalError, since customPolicy()
  calls getClass().getSimpleName() on arbitrary user policy objects
  (documented JDK edge case for certain synthetic classes). Not a bare
  Error, so OutOfMemoryError/StackOverflowError still surface.
- The load balancing policy chain walk is bounded at 16 policies and
  shared by both callers. It follows getChildPolicy() on arbitrary user
  policies, so a cyclic chain used to spin forever on the Cluster
  initialization path -- the one failure mode the try/catch cannot
  contain, because it hangs rather than throws.

A custom load balancing policy is now named after the policy the user
configured rather than PagingOptimizingLoadBalancingPolicy. Cluster
.Manager wraps every session's policy in that internal class, and it is
the outermost element of the chain, so every custom policy was reported
as {"type":"custom","name":"PagingOptimizingLoadBalancingPolicy"}. An
anonymous policy class falls back to its binary name, since it has no
simple name and the schema requires a non-empty one.

Adds a JSON-Schema conformance test suite (mirroring the 4.x sibling
PR scylladb#968): the normative schema block is shipped verbatim as a test
resource -- design-doc revision v5, whose report version field is still
1 -- and validated via com.networknt:json-schema-validator (1.5.x,
the last line still targeting Java 8), covering every discriminated-
union branch and optional group the 3.x reporter can emit. Since one
required key has no value to report, the assertion is that a report
violates the schema in exactly the documented ways and no other, with
a test naming the gap and a negative test proving
additionalProperties=false is enforced.

The report is now built behind a guard at its one call site, so a
classpath without Jackson cannot break connecting. Stage 1 made
jackson-core/jackson-databind required compile-scope dependencies of
driver-core, and DefaultDriverConfigReporter holds an ObjectMapper in a
static field: exclude jackson-databind and merely initializing that class
raises NoClassDefFoundError. That is an Error, raised while initializing
the class rather than from any method it declares, so neither the
reporter's own fail-safe nor its caller could contain it, and it happens
on the Cluster initialization path -- so a classpath that merely lacks an
optional serializer went from "the report is skipped" to "no connection
can be established", the inverse of the invariant this class is written
around. Connection.Factory.buildDriverConfigReport now catches
LinkageError -- not just NoClassDefFoundError, so a version-mismatched
Jackson surfacing as ExceptionInInitializerError is covered too, where
probing one class name would pass and still fail -- and reports nothing,
logging at WARN since reporting ships enabled and nobody opted in. Same
fallback SnappyCompressor already applies for its own optional library,
and no contradiction with buildReport() deliberately not catching bare
Error: that is about report building never masking a real JVM failure,
this is a call site tolerating a missing optional dependency.

Both node preference slots are documented as an approximation once a
wrapper sits above the policy they were read from. HostFilterPolicy
.distance() -- and so WhiteListPolicy's, which extends it -- returns
IGNORED for any host failing its predicate, including one inside the
reported datacenter, and a custom chainable policy computes distance()
itself and need honor nothing below it. The configured datacenter is
reported anyway, on the grounds that hiding one the operator really did
set is worse, and the asymmetry is deliberate: nothing is inferred on a
third party's behalf, but what was configured is passed through. The
restriction has nowhere to go, the built-in shape having no room for a
wrapper and fromDCWhiteList collapsing its datacenters into an opaque
Predicate<Host>.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants